Swift 6 & iOS E-Signature Integration: URLSession, CryptoKit & SwiftUI (2026)
How to architect data-race-free electronic signature workflows in iOS 18, macOS, and Swift Vapor 4 services using Swift 6 actor isolation, Sendable value types, SFSafariViewController Universal Link callbacks, and Apple CryptoKit HMAC-SHA256 verification.
Quick Answer (TL;DR)
Swift 6 Strict Concurrency & iOS 18 E-Signature Blueprint
To add legally binding e-signatures to an iOS 18 / Swift 6 application without shipping bloated legacy Objective-C XCFrameworks, encapsulate Signbee's POST https://signb.ee/api/v1/send call inside a backend Swift actor SignbeeClient using URLSession.shared.data(for:) and Codable & Sendable structs. Return the single-use signing_url to your SwiftUI view, present the ceremony via SFSafariViewController or ASWebAuthenticationSession with a Universal Link redirect_url, and verify X-Signbee-Signature webhooks on your server using Apple CryptoKit (HMAC<SHA256>.isValidAuthenticationCode).
1. Why Legacy Objective-C E-Signature SDKs Fail in the Swift 6 Era
With the release of Swift 6 and Xcode 16+, Apple introduced compile-time data-race safety (SWIFT_STRICT_CONCURRENCY = complete) as a first-class language guarantee. Every value crossing an isolation domain—between a @MainActor SwiftUI view model and a background network task—must conform to Sendable.
Mobile engineering teams building wealth-management apps, field-service iPad platforms, private aviation charter portals, or telehealth onboarding flows often discover that legacy enterprise e-signature iOS SDKs are frozen in an Objective-C past. Those vendor SDKs typically ship as closed-source binary .xcframework bundles that:
- Violate Swift 6 Concurrency Isolation: They rely on shared mutable singletons, untyped
[AnyHashable: Any]notification payloads, and completion handlers dispatched on unspecified Grand Central Dispatch (DispatchQueue) threads without@Sendableannotations. - Inflate App Store Download Size: Legacy iOS e-signature frameworks bundle redundant PDF rendering engines and UIKit nibs that add 22 MB to 38 MB to your uncompressed app binary—pushing users closer to cellular download warnings and degrading cold-start dyld linking time by 180 ms+.
- Create App Store Privacy Manifest Friction: Third-party binary SDKs that touch file-timestamp or user-defaults APIs without an updated, cryptographically signed
PrivacyInfo.xcprivacytrigger automated ITMS-91053 rejections when you upload builds to App Store Connect.
By integrating directly with Signbee's OpenAPI 3.1 REST endpoints using Apple's native URLSession, Codable, and SafariServices frameworks, your iOS and macOS codebase adds zero external package dependencies. For a broader look at how developer-first APIs outperform legacy enterprise suites, read our analysis of the best e-signature APIs for developers in 2026 and DocuSign API pricing explained.
| iOS / Swift Engineering Criterion | Signbee REST + Native Swift 6 | Self-Hosted Open-Source WebView | Legacy Objective-C Binary SDK |
|---|---|---|---|
| Swift 6 Strict Concurrency | 100% Native Actor & Sendable structs | Requires custom Swift wrapper | Requires @preconcurrency workarounds |
| IPA Binary Size Impact | 0 KB (Uses Foundation & SafariServices) | 0 KB on client (Heavy Docker server) | +22 MB to +38 MB XCFramework |
| App Store PrivacyInfo.xcprivacy | Zero 3rd-party SDK manifest risk | Zero 3rd-party SDK manifest risk | High ITMS-91053 rejection risk |
| Apple Silicon / VisionOS Support | iOS, iPadOS, macOS, visionOS native | WebView only | Often missing visionOS / simulator slices |
| Envelope Cost at Scale | Free tier (5/mo) • $20/mo Pro Unlimited | $90–$240/mo cloud hosting | $4.00–$8.50 per signed envelope |
2. Security Architecture: Why Your Signbee API Key Belongs on Your Backend, Not in the IPA
Before writing a single line of Swift networking code, every mobile architect must enforce a fundamental security boundary: never embed your secret signbee_live_... API key inside an iOS, iPadOS, or macOS client binary. Even if you obfuscate strings or store keys in Xcode build settings, any attacker with a jailbroken device or Proxyyman/Charles TLS interception can extract client-side bearer tokens and dispatch unauthorized contracts under your organization's identity.
Instead, production Apple ecosystem apps use a clean three-step capability token flow:
- iOS App Requests Contract Initiation: Your SwiftUI view calls your authenticated backend endpoint (e.g.,
POST https://api.yourcompany.com/v1/agreements/initiate) passing the user's session JWT and agreement parameters. - Backend
actor SignbeeClientDispatches to Signbee: Your server (running Swift Vapor 4, Hummingbird 2, or Node/Go/Kotlin) invokesPOST https://signb.ee/api/v1/sendusing your secretsignbee_live_...key and receives the single-usesigning_url. - iOS Presents the Tokenized
signing_url: Your backend returns only thesigning_urlanddocument_idto the iPhone or iPad. Because thesigning_urlis a cryptographically random, single-document capability URL that expires automatically, it is completely safe to load on the mobile device.
3. Building a Thread-Safe `actor SignbeeClient` in Swift 6
Whether you run Swift on the server with Vapor 4 / Hummingbird 2 or build an internal macOS administrative tool where Keychain-backed credentials are permitted, modeling your API client as a Swift 6 actor with Sendable value structs guarantees compile-time thread safety.
Below is the complete Swift 6 implementation of SignbeeClient. It uses JSONEncoder.KeyEncodingStrategy.convertToSnakeCase and JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase so your Swift properties remain idiomatic camelCase (senderName, signingUrl) while serializing cleanly to Signbee's snake_case wire format (sender_name, signing_url).
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct SendDocumentRequest: Codable, Sendable, Equatable {
public let title: String
public let markdown: String?
public let pdfBase64: String?
public let pdfUrl: String?
public let senderName: String
public let senderEmail: String
public let recipientName: String
public let recipientEmail: String
public let expiresInDays: Int
public let webhookUrl: String?
public let redirectUrl: String?
public let metadata: [String: String]?
public init(
title: String,
markdown: String? = nil,
pdfBase64: String? = nil,
pdfUrl: String? = nil,
senderName: String,
senderEmail: String,
recipientName: String,
recipientEmail: String,
expiresInDays: Int = 14,
webhookUrl: String? = nil,
redirectUrl: String? = nil,
metadata: [String: String]? = nil
) {
self.title = title
self.markdown = markdown
self.pdfBase64 = pdfBase64
self.pdfUrl = pdfUrl
self.senderName = senderName
self.senderEmail = senderEmail
self.recipientName = recipientName
self.recipientEmail = recipientEmail
self.expiresInDays = expiresInDays
self.webhookUrl = webhookUrl
self.redirectUrl = redirectUrl
self.metadata = metadata
}
}
public struct SendDocumentResponse: Codable, Sendable, Equatable, Identifiable {
public let id: String
public let status: String
public let title: String
public let signingUrl: URL
public let verifyUrl: URL
public let expiresAt: String
public let createdAt: String
}
public enum SignbeeClientError: Error, Sendable, LocalizedError {
case invalidResponse
case httpError(statusCode: Int, message: String)
public var errorDescription: String? {
switch self {
case .invalidResponse:
return "Received a non-HTTP response from Signbee API."
case let .httpError(statusCode, message):
return "Signbee API error (HTTP \(statusCode)): \(message)"
}
}
}
public actor SignbeeClient {
private let apiKey: String
private let baseURL: URL
private let session: URLSession
private let encoder: JSONEncoder
private let decoder: JSONDecoder
public init(
apiKey: String,
baseURL: URL = URL(string: "https://signb.ee")!,
session: URLSession = .shared
) {
self.apiKey = apiKey
self.baseURL = baseURL
self.session = session
let enc = JSONEncoder()
enc.keyEncodingStrategy = .convertToSnakeCase
self.encoder = enc
let dec = JSONDecoder()
dec.keyDecodingStrategy = .convertFromSnakeCase
self.decoder = dec
}
public func sendDocument(
_ payload: SendDocumentRequest,
idempotencyKey: String = UUID().uuidString
) async throws -> SendDocumentResponse {
let endpoint = baseURL.appendingPathComponent("/api/v1/send")
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.timeoutInterval = 15
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(idempotencyKey, forHTTPHeaderField: "Idempotency-Key")
request.httpBody = try encoder.encode(payload)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw SignbeeClientError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
let rawBody = String(data: data, encoding: .utf8) ?? "Unknown error"
throw SignbeeClientError.httpError(statusCode: httpResponse.statusCode, message: rawBody)
}
return try decoder.decode(SendDocumentResponse.self, from: data)
}
public func fetchDocumentStatus(documentId: String) async throws -> SendDocumentResponse {
let endpoint = baseURL.appendingPathComponent("/api/v1/documents/\(documentId)")
var request = URLRequest(url: endpoint)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw SignbeeClientError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
let rawBody = String(data: data, encoding: .utf8) ?? "Unknown error"
throw SignbeeClientError.httpError(statusCode: httpResponse.statusCode, message: rawBody)
}
return try decoder.decode(SendDocumentResponse.self, from: data)
}
}4. Presenting the Signing Ceremony in SwiftUI with `SFSafariViewController` & Universal Links
Once your backend returns the signingUrl to your iOS app, you have three native presentation choices on iOS 18: SFSafariViewController, ASWebAuthenticationSession, or WKWebView.
For consumer and enterprise iOS apps, wrapping SFSafariViewController in a UIViewControllerRepresentable sheet and pairing it with an Apple Universal Link (https://app.yourcompany.com/contracts/complete) in Signbee's redirect_url parameter delivers the highest conversion rate. The user sees Safari's verified HTTPS security bar, draws their signature with smooth 120Hz ProMotion touch responsiveness or Apple Pencil on iPad, taps Complete Signature, and Signbee redirects to your Universal Link—which SwiftUI intercepts via .onOpenURL to automatically dismiss the modal sheet and transition to your success screen.
If you are designing a fully customized, brand-matched signing experience, check out our guide on building a white-label e-signature API flow, or compare self-hosted alternatives in our open-source e-signature API comparison.
import SwiftUI
import SafariServices
import Observation
struct SafariSigningView: UIViewControllerRepresentable {
let signingURL: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = false
config.barCollapsingEnabled = true
let safariVC = SFSafariViewController(url: signingURL, configuration: config)
safariVC.preferredBarTintColor = UIColor(red: 0.04, green: 0.04, blue: 0.05, alpha: 1.0)
safariVC.preferredControlTintColor = UIColor(red: 0.96, green: 0.62, blue: 0.04, alpha: 1.0)
safariVC.dismissButtonStyle = .close
return safariVC
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {}
}
@Observable
@MainActor
final class ContractSigningViewModel {
var activeSigningURL: URL?
var isSigned: Bool = false
var verificationURL: URL?
var errorMessage: String?
func presentSigningCeremony(signingURL: URL, verifyURL: URL) {
self.verificationURL = verifyURL
self.activeSigningURL = signingURL
}
func handleIncomingUniversalLink(_ url: URL) {
// Matches redirect_url: https://app.yourcompany.com/contracts/complete?doc_id=...
guard url.path.contains("/contracts/complete") else { return }
self.activeSigningURL = nil
self.isSigned = true
}
}
public struct ContractSigningFlowView: View {
@State private var viewModel = ContractSigningViewModel()
let initialSigningURL: URL
let initialVerifyURL: URL
public var body: some View {
VStack(spacing: 20) {
if viewModel.isSigned {
Label("Agreement Cryptographically Signed", systemImage: "checkmark.seal.fill")
.font(.headline)
.foregroundStyle(.green)
if let verifyURL = viewModel.verificationURL {
Link("View SHA-256 Audit Certificate", destination: verifyURL)
.buttonStyle(.borderedProminent)
}
} else {
Button("Review & Sign Agreement") {
viewModel.presentSigningCeremony(
signingURL: initialSigningURL,
verifyURL: initialVerifyURL
)
}
.buttonStyle(.borderedProminent)
}
}
.sheet(item: Binding(
get: { viewModel.activeSigningURL.map { IdentifiableURL(url: $0) } },
set: { if $0 == nil { viewModel.activeSigningURL = nil } }
)) { item in
SafariSigningView(signingURL: item.url)
.ignoresSafeArea()
}
.onOpenURL { incomingURL in
viewModel.handleIncomingUniversalLink(incomingURL)
}
}
}
private struct IdentifiableURL: Identifiable {
let id = UUID()
let url: URL
}iOS Signing Container Matrix: SFSafariViewController vs. ASWebAuthenticationSession vs. WKWebView
Choosing the right iOS web container depends on whether your users sign on personal iPhones or managed multi-user iPad kiosks:
| Capability | SFSafariViewController | ASWebAuthenticationSession | WKWebView (Custom UI) |
|---|---|---|---|
| Best Use Case | Consumer & B2B iPhone/iPad apps | Custom URL scheme callback flows | In-store iPad kiosks & point-of-sale |
| Process Isolation & TLS Bar | Out-of-process + Verified HTTPS bar | Out-of-process + Verified HTTPS bar | App-controlled navigation bar |
| Callback Interception | Universal Links (.onOpenURL) | Custom scheme or Universal Link | WKNavigationDelegate policy hook |
| Ephemeral Session Option | Standard Safari Sandbox | prefersEphemeralWebBrowserSession = true | WKWebsiteDataStore.nonPersistent() |
5. Server-Side Swift: Verifying `X-Signbee-Signature` Webhooks in Vapor 4 with Apple `CryptoKit`
Even when a user completes the signing ceremony on iOS and triggers your Universal Link redirect, your backend database must treat the signed webhook from Signbee as the authoritative source of truth. A user might close Safari half a second after submitting their signature before the redirect completes, or sign the agreement from the backup email link on their MacBook instead of their iPhone.
If your backend runs on Swift Vapor 4 or Hummingbird 2, Apple's CryptoKit library (available on both macOS and Linux via swift-crypto) provides HMAC<SHA256>.isValidAuthenticationCode. Unlike naive string equality checks that are vulnerable to byte-by-byte timing side channels, isValidAuthenticationCode performs a constant-time comparison directly over the decoded hexadecimal signature bytes and the raw HTTP body buffer.
import Vapor
import Crypto
struct SignbeeWebhookPayload: Content, Sendable {
let eventId: String
let event: String
let documentId: String
let status: String
let sha256Hash: String?
let signedPdfUrl: String?
let verifyUrl: String?
let signerIp: String?
let signedAt: String?
enum CodingKeys: String, CodingKey {
case eventId = "event_id"
case event
case documentId = "document_id"
case status
case sha256Hash = "sha256_hash"
case signedPdfUrl = "signed_pdf_url"
case verifyUrl = "verify_url"
case signerIp = "signer_ip"
case signedAt = "signed_at"
}
}
enum SignbeeSignatureVerifier {
static func verify(
rawBody: ByteBuffer,
signatureHeader: String?,
webhookSecret: String
) -> Bool {
guard let signatureHeader = signatureHeader else { return false }
let hexString = signatureHeader.replacingOccurrences(of: "sha256=", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let signatureBytes = Data(hexEncoded: hexString) else {
return false
}
let payloadData = Data(buffer: rawBody)
let symmetricKey = SymmetricKey(data: Data(webhookSecret.utf8))
return HMAC<SHA256>.isValidAuthenticationCode(
signatureBytes,
authenticating: payloadData,
using: symmetricKey
)
}
}
extension Data {
init?(hexEncoded hex: String) {
guard hex.count.isMultiple(of: 2) else { return nil }
var bytes = Data(capacity: hex.count / 2)
var index = hex.startIndex
while index < hex.endIndex {
let nextIndex = hex.index(index, offsetBy: 2)
guard let b = UInt8(hex[index..<nextIndex], radix: 16) else { return nil }
bytes.append(b)
index = nextIndex
}
self = bytes
}
}
func registerSignbeeWebhookRoutes(_ app: Application, webhookSecret: String) {
app.on(.POST, "webhooks", "signbee", body: .collect(maxSize: "2mb")) { req async throws -> HTTPStatus in
guard let rawBuffer = req.body.data else {
throw Abort(.badRequest, reason: "Missing webhook request body")
}
let signatureHeader = req.headers.first(name: "X-Signbee-Signature")
guard SignbeeSignatureVerifier.verify(
rawBody: rawBuffer,
signatureHeader: signatureHeader,
webhookSecret: webhookSecret
) else {
req.logger.warning("Rejected Signbee webhook with invalid HMAC-SHA256 signature")
throw Abort(.unauthorized, reason: "Invalid X-Signbee-Signature")
}
let payload = try req.content.decode(SignbeeWebhookPayload.self)
req.logger.info("Verified Signbee event=\(payload.event) doc=\(payload.documentId) sha256=\(payload.sha256Hash ?? "pending")")
// Persist status & trigger APNs push notification to iOS device
return .ok
}
}6. Generating On-Device PDFs with `PDFKit` / SwiftUI `ImageRenderer` & Base64 Dispatch
In many iOS and iPadOS field applications—such as solar installation site surveys, automotive lease walkarounds, or clinical consent forms—the document to be signed includes annotated photos or vector diagrams rendered right on the iPad screen.
With SwiftUI's ImageRenderer (or UIGraphicsPDFRenderer), your iOS app can render any SwiftUI view hierarchy into an in-memory Data buffer, encode it via pdfData.base64EncodedString(), and pass it to your backend to submit inside Signbee's pdf_base64 field. Signbee preserves every vector path and embedded photograph, presents the exact PDF for Party B's signature, appends the A4 Audit Certificate page, and seals the combined binary with an immutable SHA-256 digest.
To see how flat-rate API pricing transforms unit economics for mobile-first startups and high-volume apps, read our guide to affordable e-signature API integration, compare every tier in our e-signature API pricing comparison (2026), and test your Xcode Simulator builds today using our free e-signature API.
7. App Store Review Guidelines (3.1.1) & Privacy Nutrition Label Compliance
iOS product managers frequently ask whether embedding contract execution inside an iPhone or iPad app triggers Apple App Store Review friction. Two compliance advantages make Signbee ideal for iOS teams:
- Exempt from Guideline 3.1.1 (In-App Purchase): Executing binding commercial contracts, equipment leases, employment agreements, or physical service orders governs real-world obligations outside digital app feature unlocking, meaning Apple never requires In-App Purchase commissions on agreements signed in your app.
- Zero Third-Party Tracking or Fingerprinting SDKs: Because your app talks to Signbee via your backend using standard
URLSessionand presents the signing link inSFSafariViewController, you ship zero third-party binary frameworks and never invoke restricted device-fingerprinting APIs.
8. Frequently Asked Questions: Swift 6 & iOS E-Signature Integration
Why do legacy Objective-C and CocoaPods e-signature SDKs fail under Swift 6 strict concurrency?
Legacy iOS e-signature SDKs built with Objective-C and distributed as binary XCFrameworks rely on mutable singleton managers, delegate callbacks on arbitrary GCD queues, and non-Sendable dictionary payloads. When compiling with Swift 6 strict concurrency checking enabled (SWIFT_STRICT_CONCURRENCY = complete), these legacy frameworks trigger dozens of data-race warnings and Sendable conformance errors. Worse, they bloat your App Store binary by 18 MB to 35 MB and frequently lack privacy nutrition manifests (PrivacyInfo.xcprivacy). Using a native Swift 6 actor and Sendable structs with Signbee's REST API requires zero third-party frameworks and passes strict concurrency verification cleanly.
Should I store my Signbee API key inside an iOS app bundle or Info.plist?
Never store your production Signbee Bearer API key (signbee_live_...) in your iOS application binary, Info.plist, or Xcode build configurations. Any string compiled into an IPA bundle can be extracted in seconds using strings, otool, or a TLS MITM proxy. Always hold your Signbee API key on your backend server (such as a Swift Vapor 4 microservice, AWS Lambda, or Node.js API). Your iOS client authenticates with your backend using the user's session token, and your backend calls Signbee's POST /api/v1/send endpoint and returns only the single-use, capability-scoped signing_url to the iOS device.
Should I use SFSafariViewController, ASWebAuthenticationSession, or WKWebView for in-app contract signing on iOS?
For most native iOS 18 and SwiftUI applications, SFSafariViewController or ASWebAuthenticationSession is strongly preferred over a raw WKWebView. SFSafariViewController runs out-of-process, provides native AutoFill and passkey support, displays a trusted TLS lock indicator that reassures users signing legal agreements, and automatically intercepts Universal Links configured in your redirect_url parameter to dismiss the sheet the moment the user finishes signing. If you require a completely chrome-free embedded layout inside an iPad kiosk app, WKWebView works seamlessly with Signbee's responsive mobile signing ceremony.
How do I verify X-Signbee-Signature webhooks in a Swift Vapor 4 backend using Apple CryptoKit?
In a Swift Vapor 4 route handler, extract the raw ByteBuffer from req.body.data and convert it to Foundation Data before decoding JSON. Parse the hex string from the X-Signbee-Signature header (stripping any sha256= prefix) into a Data array of bytes. Then call CryptoKit's HMAC<SHA256>.isValidAuthenticationCode(signatureBytes, authenticating: rawPayloadData, using: SymmetricKey(data: secretData)). Apple's CryptoKit implementation performs a constant-time byte comparison in hardware-accelerated native code, protecting your webhook endpoint from timing attacks.
Does integrating Signbee e-signatures trigger Apple App Store Guideline 3.1.1 In-App Purchase requirements?
No. Executing real-world legal contracts, B2B service agreements, residential leases, waivers, or employment onboarding documents does not unlock digital in-app content or app features, so it falls outside Apple App Store Review Guideline 3.1.1 for In-App Purchases. Furthermore, because Signbee requires zero external dependencies and collects only the statutory audit metadata required for ESIGN and eIDAS compliance (IP address, user-agent, email, and timestamp), your App Privacy nutrition label remains straightforward and passes App Review without friction.
Can I generate a PDF on-device using SwiftUI ImageRenderer or PDFKit and send it via Signbee?
Yes. While sending CommonMark markdown via Signbee's markdown field is the fastest and lowest-bandwidth path, iOS and macOS apps that render bespoke visual inspection reports, annotated architectural floorplans, or medical intake forms using PDFKit or SwiftUI's ImageRenderer can encode the generated PDF Data as a base64 string (pdfData.base64EncodedString()) and pass it to your backend to forward in the pdf_base64 field of POST /api/v1/send. Signbee seals the exact PDF bytes with a cryptographic SHA-256 certificate upon signature completion.
Add Native E-Signatures to Your iOS & Swift Stack Today
Zero CocoaPods. Zero binary bloat. Start with 5 free documents per month or unlock unlimited envelopes for $20/mo.