LayerHopDocsLaunch app (opens in a new tab)

Swift SDK

github.com/layerhop/layerhop-swift — a dependency-free Swift Package for iOS, macOS, tvOS and watchOS with async/await, Codable models, an AsyncThrowingStream watch and webhook verification.

layerhop-swift is the Swift client for the LayerHop REST API, mirroring the TypeScript SDK and the Go SDK. It is a Swift Package with no dependencies (Foundation, plus CryptoKit where available), needs iOS 15, macOS 12, tvOS 15 or watchOS 8, and every call is async throws.

The SDK never holds keys. It returns unsigned transactions; your wallet (WalletConnect / Reown AppKit, a Solana Mobile wallet, web3.swift, a KMS…) signs and broadcasts them.

How do I install it?#

In Xcode, choose File → Add Package Dependencies… and enter https://github.com/layerhop/layerhop-swift. In a Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/layerhop/layerhop-swift", from: "0.6.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [.product(name: "LayerHop", package: "layerhop-swift")]),
]

Quickstart#

Amounts are integer strings in base units ("100000000" = 100 USDC).

swift
import LayerHop

let lh = LayerHopClient(apiKey: ProcessInfo.processInfo.environment["LAYERHOP_API_KEY"])

// 1. Quote
let q = try await lh.quote(QuoteParams(
    srcChain: "base", srcToken: "USDC", amount: "100000000",
    dstChain: "solana", dstToken: "SOL",
    sender: "0xYourEvmAddress", recipient: "YourSolanaAddress"))

// 2. Build the unsigned transaction
let tx = try await lh.buildTx(quoteId: q.best.quoteId)

// 3. Sign + send with YOUR wallet
let txHash: String
switch tx {
case .evm(let evm):
    // send evm.approval first when set, then {to, data, value} on evm.chainId,
    // then each of evm.extraTxs in order
    txHash = try await wallet.sendEvm(evm)
case .svm(let svm):
    let signed = try await wallet.signSolana(base64: svm.serializedTx)
    // submitViaApi: sign only and let LayerHop broadcast
    txHash = svm.submitViaApi
        ? try await lh.submit(quoteId: q.best.quoteId, signedTransaction: signed)
        : try await wallet.broadcastSolana(signed)
}

// 4. Report the broadcast
_ = try await lh.reportSubmitted(quoteId: q.best.quoteId, txHash: txHash)

// 5. Watch until filled / refunded / failed
for try await st in lh.watch(provider: q.best.provider, id: txHash) {
    print(st.state)
}

gasOnArrival: true in QuoteParams also delivers about $2 of native gas to the recipient on an EVM destination (gasOnArrivalUsd: 5 picks the amount); check quote.gasOnArrival. See Gas on arrival.

Which methods are available?#

Method Endpoint
health() · chains() · tokens(chain:) GET /v1/health · /v1/chains · /v1/tokens
quote(_:) GET /v1/quote
buildTx(quoteId:) POST /v1/orders, returns BuiltTx (.evm(EvmTx) or .svm(SvmTx))
simulate(quoteId:) POST /v1/simulate
submit(quoteId:signedTransaction:) POST /v1/orders/submit (Solana submitViaApi only)
reportSubmitted(quoteId:txHash:) POST /v1/orders/{quoteId}/submitted
status(provider:id:) · listOrders(_:) GET /v1/orders/{provider}/{id} · GET /v1/orders
watch(provider:id:interval:timeout:) AsyncThrowingStream<OrderStatus, Error>, one item per state change; ends when final, on error, or with LayerHopError.watchTimeout (30 min by default)
streamOrder(quoteId:) · streamCheckoutSession(id:) GET /v1/orders/{quoteId}/events · /v1/checkout/sessions/{id}/events (SSE)
createCheckoutSession · getCheckoutSession · listCheckoutSessions · expireCheckoutSession · quoteCheckoutSession /v1/checkout/sessions…
createCheckoutProduct · getCheckoutProduct · listCheckoutProducts · updateCheckoutProduct · startProductCheckout /v1/checkout/products…
createRefund(sessionId:_:) · listRefunds(sessionId:) POST · GET /v1/checkout/sessions/{id}/refunds
createInvoice · getInvoice · listInvoices · updateInvoice · sendInvoice · voidInvoice · getPublicInvoice · payInvoice /v1/invoices…
createDepositAddress · getDepositAddress · listDepositAddresses · refreshDepositAddress · disableDepositAddress · simulateDeposit /v1/deposit-addresses…
createSubscription · getSubscription · listSubscriptions · updateSubscription · cancelSubscription · pauseSubscription · resumeSubscription · listSubscriptionInvoices /v1/subscriptions…
createLimitOrder · submitLimitOrder · getLimitOrder · listLimitOrders · cancelLimitOrder · simulateLimitOrder /v1/limit-orders…
serviceStatus() GET /v1/status (public)
streamWebhookEvents() · triggerWebhookEvent(type:) GET /v1/webhooks/stream (SSE) · POST /v1/webhooks/trigger (test keys)

LayerHopClient.Options: baseURL (default https://api.layerhop.com), apiKey (sent as x-api-key), channel (default api), integrator, integratorFeeBps and userAgent (default layerhop-swift/0.6.0). Pass your own URLSession as session: for custom timeouts, proxies or tests. The client is Sendable; share one instance.

How do I take payments with checkout?#

Checkout gets you paid in USDC or USDT while the customer pays with any token. Creating, listing and expiring sessions needs an API key; getCheckoutSession and quoteCheckoutSession are public.

swift
let s = try await lh.createCheckoutSession(CreateCheckoutParams(
    amount: "49000000", settleChain: "base", settleToken: "USDC", // 49 USDC
    recipient: "0xYourPayoutAddress", reference: "order_1042",
    successUrl: "https://shop.example.com/thanks"))
// open s.url; fulfil on the checkout.session.completed webhook

// Your own payment page: quote, then pay with buildTx + reportSubmitted as usual
let cq = try await lh.quoteCheckoutSession(id: s.id, CheckoutQuoteParams(srcChain: "arbitrum", srcToken: "ETH", sender: payer))
let tx = try await lh.buildTx(quoteId: cq.best.quoteId) // cq.best.srcAmount is what the payer sends

How do I sell with products?#

A product is a fixed price behind payment buttons and https://layerhop.com/buy/{id} links. In update params, only non-nil fields are sent and an empty string clears a nullable field.

swift
let p = try await lh.createCheckoutProduct(CreateCheckoutProductParams(
    name: "Pro plan", amount: "49000000", settleChain: "base", settleToken: "USDC",
    recipient: "0xYourPayoutAddress", allowedOrigins: ["https://shop.example.com"]))
_ = try await lh.updateCheckoutProduct(id: p.id, UpdateCheckoutProductParams(active: false))
// Public (no key): a session on the product's terms
let session = try await LayerHopClient().startProductCheckout(id: p.id, StartProductCheckoutParams(reference: "order_1042"))

How do I send invoices and refunds?#

Invoices bill a customer by email and are paid through checkout. Refunds are signed from your own wallet: build the returned quote, sign it from quote.from, then report the hash.

swift
let inv = try await lh.createInvoice(CreateInvoiceParams(
    lineItems: [InvoiceLineItem(description: "Design work", quantity: 2, unitAmount: "100000000")],
    settleChain: "base", settleToken: "USDC", recipient: "0xYourPayoutAddress",
    customerEmail: "jane@example.com", dueAt: "2026-11-01", send: true))
// inv.status == .open; inv.url is the hosted page

let r = try await lh.createRefund(sessionId: sessionId, CreateRefundParams(amount: "10000000", reason: "Damaged item"))
let refundTx = try await lh.buildTx(quoteId: r.quote.quoteId)
// sign and broadcast refundTx from r.quote.from, then reportSubmitted(quoteId:txHash:)

How do I use deposit addresses?#

A deposit address lets a user pay by sending tokens to an address, with no wallet connection. Each deposit becomes an order with the usual order.* webhooks. integrator and integratorFeeBps default to the client's options.

swift
let d = try await lh.createDepositAddress(CreateDepositAddressParams(
    srcChain: "base", srcToken: "USDC", dstChain: "arbitrum", dstToken: "USDC",
    recipient: "0xUserOnArbitrum", refundTo: "0xUserOnBase", label: "user_42"))
print(d.address, d.estimate?.dstAmount ?? "")
// later: refreshDepositAddress(id:) · disableDepositAddress(id:) · simulateDeposit(id:amount:) (test keys)

How do I bill subscriptions?#

Subscriptions issue and email an invoice every period; the customer pays it with any token.

swift
let sub = try await lh.createSubscription(CreateSubscriptionParams(
    customerEmail: "ada@example.com", interval: .month, productId: "prod_..."))
_ = try await lh.cancelSubscription(id: sub.id, atPeriodEnd: true)

How do I place limit orders?#

Limit orders run on Solana (beta). The created order carries an unsigned tx: have the wallet sign it (sign only), then submit it.

swift
let lo = try await lh.createLimitOrder(CreateLimitOrderParams(
    wallet: "YourSolanaAddress", srcToken: "USDC", dstToken: "SOL",
    amount: "100000000", dstAmount: "600000000"))
if let tx = lo.tx?.svm {
    let signed = try await wallet.signSolana(base64: tx.serializedTx)
    _ = try await lh.submitLimitOrder(id: lo.id, signedTransaction: signed)
}
// cancelLimitOrder(id:) returns the cancel tx to sign and submit the same way

How do I get live order status?#

streamOrder(quoteId:) reads the order's Server-Sent Events stream instead of polling and ends when the order is final. Stopping the loop or cancelling the task closes the connection.

swift
for try await order in lh.streamOrder(quoteId: quoteId) {
    print(order.status ?? "", order.dstTxHash ?? "")
}

On Linux (swift-corelibs-foundation has no URLSession.bytes) streams read the whole response before yielding.

How do I use test mode?#

Pass an lh_test_ key instead of a live one; nothing else changes. Requests are served by the sandbox provider, responses carry livemode: false, and built transactions have sandbox == true (never broadcast them; report any hash). See Test mode.

How are errors reported?#

Every failure throws LayerHopError (or CancellationError when your task is cancelled). Non-2xx responses are .api(code:message:status:requestId:); the code is one of the stable error codes. The other cases are .network(URLError), .decoding(String), .invalidResponse and .watchTimeout.

swift
do {
    _ = try await lh.buildTx(quoteId: quoteId)
} catch let e as LayerHopError where e.code == "QUOTE_EXPIRED" {
    // re-quote
}

How do I verify webhooks?#

Pass the raw body exactly as received, before any JSON parsing.

swift
do {
    let event = try Webhook.constructEvent(
        secret: webhookSecret, body: rawBody, header: request.value(forHTTPHeaderField: "LayerHop-Signature"))
    if event.type == .orderFilled { /* … */ }
} catch {
    // respond 401
}

The header is t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>; any matching v1 value is accepted and the default tolerance is 300 s. Webhook.verifySignature throws WebhookSignatureError.malformedHeader, .timestampOutsideTolerance or .signatureMismatch; Webhook.isValidSignature returns a Bool, and Webhook.sign builds a header for testing your handler. See Webhooks.

What else should I know?#

The chain-asset endpoints (GET /v1/tokens/lookup, GET /v1/prices and GET /v1/balances) have no wrapper in this SDK yet; call them over HTTP. See Chains, tokens, prices and balances.

Last updated