# 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.

Source: https://docs.layerhop.com/sdks/swift

`layerhop-swift` is the Swift client for the LayerHop [REST API](/api-reference), mirroring the [TypeScript SDK](/sdk) and the [Go SDK](/sdks/go). 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](/guides/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](/guides/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](/guides/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](/guides/invoices) bill a customer by email and are paid through checkout. [Refunds](/guides/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](/guides/deposit-addresses) 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](/guides/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](/guides/limit-and-recurring-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](/guides/live-status) 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](/guides/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](/errors). 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](/guides/webhooks).

## What else should I know?

The chain-asset endpoints ([`GET /v1/tokens/lookup`](/api-reference#look-up-a-token), [`GET /v1/prices`](/api-reference#get-usd-prices) and [`GET /v1/balances`](/api-reference#get-wallet-balances)) have no wrapper in this SDK yet; call them over HTTP. See [Chains, tokens, prices and balances](/guides/tokens-and-balances).
