> For the complete documentation index, see [llms.txt](https://docs.swapkit.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.swapkit.dev/swapkit-api/v3-limit-place-and-manage-limit-orders.md).

# /v3/limit - Place and manage limit orders

{% hint style="warning" %}
**Pre-release.** This API is in active development and request/response schemas may still change before GA. Only test with small amounts — orders placed here route real liquidity through our providers, so any funds committed are at real risk while the service stabilises.
{% endhint %}

The **SwapKit Limit Order API** lets integrators offer their users price-contingent swaps: the order rests until the market reaches the target price, then settles asynchronously. All endpoints sit under `/v3/limit/*`, every request must carry an `x-api-key` header, and all payloads are JSON.

Integrators **do not choose the provider**. SwapKit routes the pair server-side and returns the chosen provider on the quote. The wallet-signing shape differs per provider, and the service surfaces that difference on the `/v3/limit/build` response.

***

### Signing models

Every provider fits one of two signing models. `/v3/limit/build` tells you which one applies to a given order by populating exactly one set of fields:

| Model             | Build response                                  | What the wallet does                                                                                                                                         |
| ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Signed intent** | `typedData` populated, `tx` and `txMeta` `null` | Signs an off-chain payload. Nothing goes on-chain at build time — the provider holds the signed order until a filler matches it.                             |
| **Deposit**       | `tx` and `txMeta` populated, `typedData` `null` | Signs and broadcasts a transaction. The deposit **is** the order: its parameters are encoded into the transaction, and the resulting tx hash is the receipt. |

{% hint style="info" %}
**Branch on the artifact, not the provider.** Check `typedData !== null` instead of switching on `provider`. Every provider maps onto one of these two models — including ones not yet live — so your integration keeps working as new providers ship.
{% endhint %}

### Providers

| Provider      | Signing model       | Coverage                              | Chains                                                    | Status                                                  |
| ------------- | ------------------- | ------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------- |
| **1inch**     | Signed intent       | Same-chain EVM swaps                  | ETH, ARB, AVAX, BASE, BSC                                 | Available — more EVM chains rolling out                 |
| **Harbor**    | Deposit             | Cross-chain between BTC, ETH and USDT | BTC, ETH (+ USDT)                                         | Available — BTC ↔ ETH.ETH and BTC ↔ ETH.USDT pairs      |
| **THORChain** | Confirmed at launch | Native cross-chain across 11 chains   | BTC, ETH, LTC, ATOM, DOGE, AVAX, BSC, SOL, BASE, TRX, XRP | Coming soon — broadens BTC, LTC, Cosmos, Doge, TRX, XRP |
| **Jupiter**   | Confirmed at launch | Solana native assets                  | SOL                                                       | Coming soon — Solana spot liquidity                     |

New providers are added without a breaking change: the endpoints, the order object, and the status lifecycle stay identical, and the only per-provider variation is which signing model the `/build` response uses.

***

### Endpoint index

| Method | Endpoint                    | Description                                                                                         |
| ------ | --------------------------- | --------------------------------------------------------------------------------------------------- |
| `GET`  | `/v3/limit/tokens`          | Supported assets and pairs per provider.                                                            |
| `POST` | `/v3/limit/quote`           | Price the pair, reserve a `routeId`. [See below](#id-1.-price-the-pair).                            |
| `POST` | `/v3/limit/build`           | Persist the order and produce the signing artifact. [See below](#id-2.-build-the-signing-artifact). |
| `POST` | `/v3/limit/submit`          | Relay the wallet signature or deposit tx hash. [See below](#id-3.-submit-the-signed-artifact).      |
| `GET`  | `/v3/limit/orders/:orderId` | Single order detail, refreshed from the provider. [See below](#id-4.-single-order-detail).          |
| `GET`  | `/v3/limit/orders`          | Paginated list scoped to the API key. [See below](#id-5.-paginated-order-list).                     |
| `POST` | `/v3/limit/cancel/build`    | Pre-build a cancel transaction. [See below](#id-6.-cancel-an-order).                                |
| `POST` | `/v3/limit/cancel/submit`   | Record the broadcast cancel tx or signature. [See below](#id-7.-submit-the-cancellation).           |

***

## 1. Price the pair

**Method:** `POST`\
**URL:** `https://api.swapkit.dev/v3/limit/quote`

Returns a market price for the pair, computes the user's `limitPrice` deviation from spot, and caches a `routeId` for the subsequent `/v3/limit/build` call.

### Request schema

<table><thead><tr><th width="193.56640625">Parameter</th><th width="208.23828125">Type</th><th width="99.421875">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>sellAsset</code></td><td><code>string</code></td><td>Yes</td><td>The asset being sold (e.g. <code>"ETH.ETH"</code>)</td></tr><tr><td><code>buyAsset</code></td><td><code>string</code></td><td>Yes</td><td>The asset being bought (e.g. <code>"BTC.BTC"</code>).</td></tr><tr><td><code>sellAmount</code></td><td><code>decimal string</code></td><td>No</td><td>Provide <strong>any two</strong> of <code>sellAmount</code> / <code>buyAmount</code> / <code>limitPrice</code>; the third is derived. <strong>Human-readable decimal, not base units</strong> — selling 10 USDT is <code>"10"</code>, not <code>"10000000"</code>.</td></tr><tr><td><code>buyAmount</code></td><td><code>decimal string</code></td><td>No</td><td>Derived from the other two if omitted. Same human-readable decimal convention as <code>sellAmount</code>.</td></tr><tr><td><code>limitPrice</code></td><td><code>decimal string</code></td><td>No</td><td><code>buyAsset</code> per 1 <code>sellAsset</code>, in human-readable decimal.</td></tr><tr><td><code>sourceAddress</code></td><td><code>string</code></td><td>No</td><td>Screened for AML at quote time and pre-filled into the <code>/v3/limit/build</code> hint.</td></tr><tr><td><code>destinationAddress</code></td><td><code>string</code></td><td>No</td><td>Same treatment as <code>sourceAddress</code>.</td></tr><tr><td><code>affiliateFee</code></td><td><code>int</code> (0–1000 bps)</td><td>No</td><td>Overrides the API key default.</td></tr><tr><td><code>expiresAt</code></td><td><code>int</code> (unix seconds)</td><td>No</td><td>Defaults to now + 3 days.</td></tr></tbody></table>

{% hint style="info" %}
Asset identifiers follow the same nomenclature as the rest of the API — `Chain.Asset` (`"BTC.BTC"`) or `Chain.Asset-ContractAddress` (`"ETH.USDC-0xA0b8…"`). See [`/tokens`](/swapkit-api/tokens-list-and-search-supported-tokens.md).
{% endhint %}

### Response schema

**Top level**

| Field     | Type           | Description                  |
| --------- | -------------- | ---------------------------- |
| `quoteId` | `string`       | UUID for this quote response |
| `routes`  | `LimitRoute[]` | A single-entry array.        |

**Per route**

<table><thead><tr><th width="221.390625">Field</th><th width="201.84375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>routeId</code></td><td><code>string</code></td><td>UUID of this specific swap response. Pass this to <code>/v3/limit/build</code>.</td></tr><tr><td><code>provider</code></td><td><code>enum</code></td><td>Provider used, chosen server-side.</td></tr><tr><td><code>sellAmount</code>, <code>buyAmount</code>, <code>limitPrice</code></td><td><code>decimal string</code></td><td>Resolved values, including the leg you didn't provide.</td></tr><tr><td><code>spotPrice</code></td><td><code>decimal string</code></td><td>Current market reference price.</td></tr><tr><td><code>effectiveFillPrice</code></td><td><code>decimal string</code></td><td>The price the market must reach for the order to fill: <code>spotPrice × (1 + feeGapBps / 10_000)</code>. The gap is <code>takerFeeBps + integratorFeeBps</code> on 1inch, where the integrator fee is charged on the taker side; on Harbor it's <code>takerFeeBps</code> alone, since the integrator fee is settled out of proceeds and doesn't affect the fill threshold. Equals <code>spotPrice</code> when the gap is zero. </td></tr><tr><td><code>takerFeeBps</code></td><td><code>number</code></td><td>Protocol-side taker fee in bps a whitelisted filler pays on top of the taking amount. Excludes the integrator fee, which is reported separately as <code>integratorFeeBps</code>.</td></tr><tr><td><code>spotPriceDeviationBps</code></td><td><code>number</code></td><td>Signed bps delta of <code>limitPrice</code> vs <strong><code>effectiveFillPrice</code></strong> — not vs raw <code>spotPrice</code>. <br><strong>Positive</strong>: the limit sits above the fill threshold, so the order waits for the market to move. <br><strong>Negative</strong>: it's at or below the threshold and would fill immediately at a worse rate than a market swap.</td></tr><tr><td><code>minExpirationSeconds</code>, <code>maxExpirationSeconds</code></td><td><code>number</code></td><td>Bounds for the <code>expiresAt</code> you may pass to <code>/v3/limit/build</code>.</td></tr><tr><td><code>integratorFeeBps</code></td><td><code>number</code></td><td>Affiliate fee applied to this route.</td></tr><tr><td><code>warnings</code></td><td><code>Warning[]</code></td><td>Structured warning objects — <a href="#warnings">see below</a>.</td></tr><tr><td><code>nextActions</code></td><td><code>object[]</code></td><td>Data needed for the next request in the flow (<code>/v3/limit/build</code> call).</td></tr></tbody></table>

Compare against `effectiveFillPrice`, not `spotPrice` — it's the threshold that triggers a fill, and `spotPriceDeviationBps` is measured from it.&#x20;

`expiresAt` is an absolute unix timestamp, but the bounds are durations: `expiresAt − now` must fall within `[minExpirationSeconds, maxExpirationSeconds]`. Outside that, `/build` returns `limitOrderExpirationOutOfBounds` (400).

### Warnings

Each entry in `warnings[]` is a structured object — the same shape swap quotes already use — so you can render a short `display` label with a longer `tooltip` behind it. `tooltip` is optional; null-check it. The array is always present and may be empty, and the cached route carries its warnings through to the `/v3/limit/build` response as well.

At most one price warning fires per quote. Both are measured against `effectiveFillPrice`, not `spotPrice`:

* `limitPriceBelowSpot` — `spotPriceDeviationBps ≤ -100`. The limit sits 1% or more below the effective fill price, so the order fills immediately at a clearly worse rate than a market swap.
* `limitPriceWithinFeeGap` — `-100 < spotPriceDeviationBps ≤ 0`, and the fee gap is non-zero. The limit sits inside the fee gap: it still fills immediately, but only modestly off-market. Raising the limit by `|spotPriceDeviationBps|` bps or more puts it above the threshold, where it rests as a genuine limit order.

Both mean the order fills on submission rather than resting — surface either to the user before they sign. Render the `display` string with `tooltip` behind it rather than switching on `code`, and treat `code` as an open enum: new values ship with new providers.

### Errors

| Status | Error code                  | Scenario                                                                                                                                                             |
| ------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `insufficientLiquidity`     | The provider could not return a market rate for the pair, or the market rate / requested amount evaluated to zero. Surfaced as *"Insufficient liquidity for trade"*. |
| 400    | `invalidSourceAddress`      | `sourceAddress` was provided but doesn't match the sell-chain address format. Per-chain validation fires at `/quote` rather than waiting for `/build`.               |
| 400    | `invalidDestinationAddress` | `destinationAddress` was provided but doesn't match the buy-chain address format. Same early-validation behaviour.                                                   |
| 401    | `apiKeyInvalid`             | Missing or unrecognized `x-api-key` header.                                                                                                                          |

***

## 2. Build the signing artifact

**Method:** `POST`\
**URL:** `https://api.swapkit.dev/v3/limit/build`

Validates and screens both addresses per-chain, resolves the token spender where the chain needs one, and runs the provider build, deep address screens, and approval check in parallel. Persists the order with `status = PENDING`.

### Request schema

<table><thead><tr><th width="211.0390625">Parameter</th><th width="146.75390625">Type</th><th width="113.75">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>routeId</code></td><td><code>string</code></td><td>Yes</td><td>The ID of the route to build the order from. Obtained from a previous <code>/v3/limit/quote</code> response</td></tr><tr><td><code>sourceAddress</code></td><td><code>string</code></td><td>Yes</td><td>Blockchain address to send the asset from. Must be a valid address for the sell asset's chain. Becomes the order's <code>maker</code>.</td></tr><tr><td><code>destinationAddress</code></td><td><code>string</code></td><td>Yes</td><td>Recipient blockchain address to send the asset to. Must be a valid address for the buy asset's chain. Becomes the order's <code>receiver</code>.</td></tr><tr><td><code>expiresAt</code></td><td><code>int</code> (unix seconds)</td><td>Yes</td><td>Must fall inside <code>[minExpirationSeconds, maxExpirationSeconds]</code> from <code>/v3/limit/quote</code>.</td></tr><tr><td><code>allowPartialFill</code></td><td><code>bool</code></td><td>No</td><td>1inch only, and must be <code>true</code> (the default). Disabling either flag forces LOP v6 bit-invalidator mode, which the orderbook accepts only for RFQ orders, so <code>/build</code> returns <code>limitOrderUnsupportedFillFlags</code> (400). Harbor ignores both.</td></tr><tr><td><code>allowMultipleFills</code></td><td><code>bool</code></td><td>No</td><td>Same constraint as <code>allowPartialFill</code></td></tr><tr><td><code>usePermit2</code></td><td><code>bool</code></td><td>No</td><td><strong>Currently ignored server-side</strong> — a placeholder for an upcoming Permit2 two-phase build flow on EVM. Until it ships, EVM token orders use the <a href="#the-approvaltx-object"><code>approvalTx</code> path</a>. Default <code>false</code>.</td></tr></tbody></table>

### Build response schema

<table><thead><tr><th width="187.5">Field</th><th width="190.875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>orderId</code></td><td><code>string</code></td><td>UUID of this specific order. Pass it to <code>/v3/limit/submit</code> and every later call.</td></tr><tr><td><code>orderHash</code></td><td><code>string</code></td><td>Canonical on-chain identifier of the order, <code>0x</code>-prefixed.</td></tr><tr><td><code>typedData</code></td><td><code>object</code> (optional)</td><td>Off-chain payload for the wallet to sign. <strong>Signed-intent orders only</strong> — <code>null</code> on deposit orders. <a href="#the-typeddata-object">See below</a>.</td></tr><tr><td><code>tx</code></td><td>varies (optional)</td><td>Ready-to-sign deposit transaction. <strong>Deposit orders only</strong> — <code>null</code> on signed-intent orders. <a href="#the-tx-field">See below</a>.</td></tr><tr><td><code>txMeta</code></td><td><code>object</code> (optional)</td><td>Broadcast hints for <code>tx</code>. <strong>Deposit orders only</strong> — <code>null</code> on signed-intent orders. <a href="#the-tx-field">See below</a>.</td></tr><tr><td><code>isApproved</code></td><td><code>boolean</code> (optional)</td><td>Whether the sell-side token allowance is already in place. Omitted when no approval is part of the flow — <a href="#the-isapproved-field">See below</a>.</td></tr><tr><td><code>approvalTx</code></td><td><code>object</code> (optional)</td><td>Present when a token approval transaction must be submitted before the order can be signed or broadcast. <a href="#the-approvaltx-object">See below</a>.</td></tr><tr><td><code>warnings</code></td><td><code>Warning[]</code></td><td>Potential warnings about this order, carried over from <code>/v3/limit/quote</code>. Warnings never block the response.</td></tr><tr><td><code>nextActions</code></td><td><code>object[]</code></td><td>Data needed for the next request in the flow (<code>/v3/limit/submit</code> call).</td></tr></tbody></table>

### Response — the two signing models

You always get exactly one of the two shapes below, never both and never a mix — see [signing models](#signing-models).

{% tabs %}
{% tab title="Signed intent — typedData to sign" %}
**1inch today.**

```json
{
  "orderId": "ord_…",
  "orderHash": "0x…",
  "typedData": {
    "domain":      { },
    "types":       { "Order": [] },
    "primaryType": "Order",
    "message":     { }
  },
  "tx":     null,
  "txMeta": null,
  "isApproved": true,
  "warnings":   [],
  "nextActions": []
}
```

The wallet signs `typedData`, then you post the resulting signature to `/v3/limit/submit` as `{ orderId, signature }`.
{% endtab %}

{% tab title="Deposit — tx to broadcast" %}
**Harbor today.**

```json
{
  "orderId": "ord_…",
  "orderHash": "0x…",
  "typedData": null,
  "tx": {
    "from":  "0x…",
    "to":    "0x…",
    "value": "…",
    "data":  "0x…"
  },
  "txMeta": {
    "txType":  "evm",
    "chainId": "1",
    "memo":    "o:…"
  },
  "isApproved": true,
  "warnings":   [],
  "nextActions": []
}
```

The wallet signs and broadcasts `tx`, then you post the on-chain hash to `/v3/limit/submit` as `{ orderId, depositTxHash }`.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Why the two models exist.** They reflect two kinds of venue. An off-chain orderbook can hold a signed order until a filler matches it, so a signature is all it needs from the user. A deposit venue has no off-chain book to hold anything — the order doesn't exist until it's on-chain — so the deposit transaction has to carry the order parameters itself. 1inch is the first venue of the former kind, Harbor of the latter.
{% endhint %}

#### The `typedData` object

Returned for signed-intent orders only. A structured payload the wallet signs without broadcasting — today an EIP-712 document with `domain`, `types`, `primaryType`, and `message`. Pass it to the wallet unmodified and post the resulting signature to `/v3/limit/submit`; don't reconstruct or reorder it, since the signature is taken over the exact payload.

#### The `tx` field

Returned for deposit orders only. Its shape depends on the sell chain, and `txMeta` tells you how to handle it — the same pattern `/v3/swap` uses:

<table><thead><tr><th width="186.359375">Field</th><th width="159.66796875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>txMeta.txType</code></td><td><code>string</code></td><td>How to sign and broadcast <code>tx</code>. <code>"evm"</code> — <code>tx</code> is an object with <code>{ from, to, value, data }</code>. <code>"psbt"</code> — <code>tx</code> is a base64 PSBT string. More values ship with new chains.</td></tr><tr><td><code>txMeta.chainId</code></td><td><code>string</code></td><td>Chain the deposit is broadcast on. Numeric-string for EVM (<code>"1"</code>), slug for non-EVM (<code>"bitcoin"</code>).</td></tr><tr><td><code>txMeta.memo</code></td><td><code>string</code></td><td>The order memo encoded into the deposit, exposed so you can verify it before signing. Present for providers that encode order parameters in a memo.</td></tr></tbody></table>

#### The `approvalTx` object

Present when the sell asset is a token on a chain that requires an allowance to a spender contract, and the maker's current allowance is below the order amount. Same shape as the `approvalTx` returned by [`/v3/swap`](/swapkit-api/v3-swap-obtain-swap-transaction-details.md#the-approvaltx-object) — broadcast it and wait for confirmation **before** signing or broadcasting the order.

<table><thead><tr><th width="216.56640625">Field</th><th width="129.50390625">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>approvalTx.to</code></td><td><code>string</code></td><td>Token contract address to send the approval</td></tr><tr><td><code>approvalTx.from</code></td><td><code>string</code></td><td>User wallet address.</td></tr><tr><td><code>approvalTx.value</code></td><td><code>string</code></td><td>ETH value — always <code>"0"</code> for approvals.</td></tr><tr><td><code>approvalTx.data</code></td><td><code>string</code></td><td>Encoded <code>approve()</code> call data.</td></tr><tr><td><code>approvalTx.gasLimit</code></td><td><code>string</code></td><td>Optional. Estimated max gas units for the approval transaction, as a hex quantity string</td></tr><tr><td><code>approvalTx.gasPrice</code></td><td><code>string</code></td><td>Optional. Gas price in wei per gas unit, as a hex quantity string. Legacy (type-0) gas pricing.</td></tr></tbody></table>

#### The `isApproved` field

`isApproved` is only populated when a separate token `approve()` is part of the flow. Treat it as a tri-state:

<table><thead><tr><th width="268.59765625">Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>isApproved: true</code></td><td>An approval is required for this order and the allowance is already in place. Nothing extra to broadcast.</td></tr><tr><td><code>isApproved: false</code></td><td>An approval is required and the wallet must broadcast the accompanying <a href="#the-approvaltx-object"><code>approvalTx</code></a> <strong>before</strong> signing the order.</td></tr><tr><td>Field omitted</td><td>No approval is required — the sell asset is a native asset, or the chain has no allowance model (UTXO chains, for instance).</td></tr></tbody></table>

Whether the field appears is a property of the **sell asset and its chain**, not of the provider.

{% hint style="warning" %}
Treat a missing `isApproved` as "nothing to broadcast", **not** as `false`.
{% endhint %}

***

## 3. Submit the signed artifact

**Method:** `POST`\
**URL:** `https://api.swapkit.dev/v3/limit/submit`

Attaches the wallet signature (signed-intent orders) or the on-chain deposit tx hash (deposit orders) to the order, advancing it from `PENDING` to `SUBMITTED`. The poller then watches for provider acknowledgement and flips `SUBMITTED` → `OPEN`.

### Request schema

Send `signature` **or** `depositTxHash`, depending on which artifact `/v3/limit/build` returned.

<table><thead><tr><th width="142.90234375">Parameter</th><th width="131.2578125">Type</th><th width="153.3828125">Required for</th><th>Description</th></tr></thead><tbody><tr><td><code>orderId</code></td><td><code>string</code></td><td>All orders</td><td>The ID of the order to submit. Obtained from a previous <code>/v3/limit/build</code> response</td></tr><tr><td><code>signature</code></td><td><code>string</code></td><td>Signed intent</td><td>Wallet signature over the <code>typedData</code> returned by <code>/v3/limit/build</code>. Relayed to the provider's orderbook.</td></tr><tr><td><code>depositTxHash</code></td><td><code>string</code></td><td>Deposit</td><td>Hash of the broadcast deposit transaction. Accepted case-insensitively and stored lowercase — it surfaces back as <code>depositHash</code> on <code>/v3/limit/orders/:orderId</code>.</td></tr></tbody></table>

```json
// Signed intent — wallet signature over typedData
{ "orderId": "ord_…", "signature": "0x…" }

// Deposit — hash of the broadcast deposit transaction
{ "orderId": "ord_…", "depositTxHash": "0x… | …btc txid…" }
```

### Submit response schema

<table><thead><tr><th width="180.109375">Field</th><th width="198.86328125">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>orderId</code></td><td><code>string</code></td><td>UUID of this specific order.</td></tr><tr><td><code>orderHash</code></td><td><code>string</code></td><td>Canonical on-chain identifier of the order, <code>0x</code>-prefixed.</td></tr><tr><td><code>status</code></td><td><code>enum</code></td><td>Always <code>SUBMITTED</code> on success. See the <a href="#order-status-lifecycle">order status lifecycle</a>.</td></tr><tr><td><code>createdAt</code></td><td><code>ISO 8601 string</code></td><td>When the order was created.</td></tr></tbody></table>

{% hint style="warning" %}
**One shot — the order must still be `PENDING`.** Re-submitting an order, submitting one that has already advanced to `SUBMITTED` / `OPEN` / `FILLED`, or submitting one the stale-unsubmitted sweep has moved to `EXPIRED`, all return `409` with error code `limitOrderInvalidState`. Call `/build` and `/submit` back-to-back — the grace window before the sweep fires is 1 hour by default.
{% endhint %}

***

## 4. Single-order detail

**Method:** `GET`\
**URL:** `https://api.swapkit.dev/v3/limit/orders/:orderId`

Returns one order. Syncs with the upstream provider on every call before returning, so the response always reflects post-sync state.

### Order object

<table><thead><tr><th width="181.5546875">Field</th><th width="185.2109375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>orderId</code></td><td><code>UUID string</code></td><td>UUID of this specific order. Stable across the order's lifetime.</td></tr><tr><td><code>orderHash</code></td><td><code>string</code></td><td>Canonical order identifier, <code>0x</code>-prefixed, stable for the order's whole lifetime. <strong>Signed-intent orders</strong>: the digest of the signed payload. <strong>Deposit orders</strong>: a deterministic hash over the order parameters — it is <strong>not</strong> replaced by the deposit tx hash after <code>/submit</code>; that hash surfaces separately as <code>depositHash</code>.</td></tr><tr><td><code>chainId</code></td><td><code>string enum</code></td><td><code>ChainId</code> of the sell asset. Numeric-string for EVM (<code>"1"</code>, <code>"42161"</code>, <code>"8453"</code>, …); slug for non-EVM (<code>"bitcoin"</code>, <code>"solana"</code>, <code>"thorchain-1"</code>, …).</td></tr><tr><td><code>provider</code></td><td><code>enum</code></td><td>Provider that routed this order.</td></tr><tr><td><code>maker</code></td><td><code>string</code></td><td>Blockchain address to send the asset from — the address that owns the order on the sell chain. Format varies per chain (EVM hex, BTC bech32, etc.).</td></tr><tr><td><code>receiver</code></td><td><code>string</code> or <code>null</code></td><td>Recipient blockchain address to send the asset to on the buy chain. Nullable only for legacy orders created without one.</td></tr><tr><td><code>sellAsset</code></td><td><code>string</code></td><td>The asset being sold (e.g. <code>"ETH.ETH"</code>)</td></tr><tr><td><code>buyAsset</code></td><td><code>string</code></td><td>The asset being bought (e.g. <code>"BTC.BTC"</code>).</td></tr><tr><td><code>sellAmount</code></td><td><code>decimal string</code></td><td>Amount of the sell asset. <strong>Human-readable decimal, not smallest units</strong> — unlike <code>/v3/quote</code> and <code>/v3/swap</code>.</td></tr><tr><td><code>buyAmount</code></td><td><code>decimal string</code></td><td>Amount of the buy asset the order is resting for. Same human-readable decimal convention as <code>sellAmount</code>.</td></tr><tr><td><code>limitPrice</code></td><td><code>decimal string</code></td><td><code>buyAsset</code> per 1 <code>sellAsset</code>, in human-readable decimal.</td></tr><tr><td><code>filledSellAmount</code>, <code>filledBuyAmount</code></td><td><code>decimal string</code></td><td><code>"0"</code> before any fill. On providers that settle all-or-nothing they jump straight to the full amounts; on providers that support partial fills they climb incrementally.</td></tr><tr><td><code>txHashes</code></td><td><code>TxHash[]</code></td><td>On-chain transactions in the order's lifecycle, chronological. Always present; <code>[]</code> before anything lands. Populated by provider sync or backfill.</td></tr><tr><td><code>usdValueOpen</code></td><td><code>string</code> or <code>null</code></td><td>Sell-side notional in USD at build time. Approximate (cached price); <code>null</code> if the lookup missed.</td></tr><tr><td><code>usdValueClose</code></td><td><code>string</code> or <code>null</code></td><td>Realized buy-side notional in USD at the terminal transition. Same approximation; <code>null</code> until the order is terminal.</td></tr><tr><td><code>status</code></td><td><code>enum</code></td><td><code>PENDING</code>, <code>SUBMITTED</code>, <code>OPEN</code>, <code>PARTIAL</code>, <code>FILLED</code>, <code>CANCELLED</code>, <code>EXPIRED</code>, <code>FAILED</code>. See the <a href="#order-status-lifecycle">order status lifecycle</a>.</td></tr><tr><td><code>fees</code></td><td><code>Fee[]</code></td><td>List of fees applied to the order (liquidity, affiliate, service, network) — <a href="#fees-breakdown">see below</a>. Pre-fill: projected amounts. Post-fill: actual settled values where the provider reports them.</td></tr><tr><td><code>depositHash</code></td><td><code>string?</code></td><td>L1 tx hash of the deposit, lowercased. Chain-native format (<code>0x</code>-prefixed on EVM, raw 64-hex on Bitcoin). Populated by the provider sync once the deposit lands; omitted until then, and on models with no deposit leg.</td></tr><tr><td><code>withdrawHash</code></td><td><code>string?</code></td><td>L1 tx hash of the settlement (withdraw), lowercase <code>0x</code>-prefixed. Populated after the order fills; omitted before.</td></tr><tr><td><code>expiresAt</code></td><td><code>ISO 8601 string</code></td><td>TTL. SwapKit flips the order to <code>EXPIRED</code> locally once <code>expiresAt</code> is in the past; providers that escrow funds refund them at this point.</td></tr><tr><td><code>createdAt</code>, <code>updatedAt</code></td><td><code>ISO 8601 string</code></td><td>Server timestamps. <code>updatedAt</code> moves every time sync writes new state.</td></tr></tbody></table>

### Fees breakdown

Fees are categorized into different types based on their role in the order's lifecycle. Not every provider charges every type.

<table><thead><tr><th width="215.77734375">Fee Type</th><th>Description</th></tr></thead><tbody><tr><td><strong>Liquidity</strong></td><td>Fee applied by the liquidity provider to facilitate the swap.</td></tr><tr><td><strong>Affiliate</strong></td><td>Fee paid to the specified affiliate, projected from the API key config.</td></tr><tr><td><strong>Service</strong></td><td>SwapKit's service fee. Currently <code>0</code>.</td></tr><tr><td><strong>Network</strong></td><td>Blockchain transaction fee for processing the order — destination-chain gas / outbound fee.</td></tr></tbody></table>

Each entry in `fees[]` has the following shape:

<table><thead><tr><th width="166.53125">Field</th><th width="178.91796875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>type</code></td><td><code>enum</code></td><td><code>liquidity</code>, <code>affiliate</code>, <code>service</code> or <code>network</code>.</td></tr><tr><td><code>amount</code></td><td><code>string</code></td><td>Fee amount in human-readable decimal.</td></tr><tr><td><code>amountBps</code></td><td><code>number</code></td><td>Fee in basis points (100 bps = 1%).</td></tr><tr><td><code>asset</code></td><td><code>string</code></td><td>SwapKit asset identifier the fee is denominated in.</td></tr><tr><td><code>chain</code></td><td><code>string</code></td><td>Chain where the fee is paid or extracted.</td></tr><tr><td><code>protocol</code></td><td><code>enum</code></td><td>Provider that charges the fee.</td></tr></tbody></table>

```json
{
  "type":      "liquidity",
  "amount":    "0.00123",
  "amountBps": 30,
  "asset":     "ETH.USDT-0x…",
  "chain":     "ETH",
  "protocol":  "HARBOR"
}
```

{% hint style="info" %}
**Projected vs settled fees.** Before a fill, `fees[]` is a build-time projection priced on the sell chain. Once the order fills, providers that report settlement figures — Harbor today — replace their `liquidity` and `network` entries with actual values, and each replaced entry's `chain` becomes the fee asset's chain (`"ETH"` for a USDT fee). `affiliate` and `service` stay projected. So don't assume an entry's `chain` holds steady across the order's lifetime, and keep projected and settled entries apart when totalling per chain.

**Multi-affiliate encoding.** When an API key carries both a SwapKit fee and an integrator fee, providers that support two recipients encode both — Harbor's `o:` memo lists SwapKit first: `…:sk/<integrator>:<skBps>/<integratorBps>`. Where the orderbook takes only one recipient (1inch today), the SwapKit slot is fixed at 0.
{% endhint %}

### Errors

<table><thead><tr><th width="120.71484375">Status</th><th width="211.43359375">Error code</th><th>Scenario</th></tr></thead><tbody><tr><td>404</td><td><code>limitOrderNotFound</code></td><td>Unknown <code>orderId</code>, or the order belongs to a different API key. Ownership is enforced — the response does not leak existence across keys.</td></tr><tr><td>401</td><td><code>apiKeyInvalid</code></td><td>Missing or unrecognized <code>x-api-key</code> header.</td></tr></tbody></table>

***

## 5. Paginated order list

**Method:** `GET`\
**URL:** `https://api.swapkit.dev/v3/limit/orders`

Returns a paginated list of orders owned by the requesting API key, newest first. It does **not** re-sync with the provider on each call — use [`/v3/limit/orders/:orderId`](#id-4.-single-order-detail) when freshness matters.

### Query parameters

All optional. Filters are AND-combined except for `sourceAddress` / `destinationAddress`, whose join is controlled by `joinType`. Unknown query keys are silently ignored. API-key scoping is always applied — you cannot see another partner's orders even by querying a known address.

<table><thead><tr><th width="205.73046875">Parameter</th><th width="167.65625">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>sourceAddress</code></td><td><code>string</code></td><td>Filter by the blockchain address the asset is sold from (<code>Order.maker</code>). EVM addresses match case-insensitively, so checksummed and lowercase forms return the same orders. Non-EVM addresses (base58, bech32) match exactly, since their casing is significant.</td></tr><tr><td><code>destinationAddress</code></td><td><code>string</code></td><td>Filter by the recipient blockchain address (<code>Order.receiver</code>). Same casing rules as <code>sourceAddress</code>.</td></tr><tr><td><code>sourceChain</code></td><td><code>string enum</code></td><td>Filter by the <em>sell-asset</em> chain. Stringified EVM ids (<code>"1"</code>, <code>"42161"</code>, <code>"8453"</code>, …) or a non-EVM slug (<code>"bitcoin"</code>, <code>"solana"</code>, <code>"thorchain-1"</code>).</td></tr><tr><td><code>destinationChain</code></td><td><code>string enum</code></td><td>Filter by the <em>buy-asset</em> chain. Same value space as <code>sourceChain</code>.</td></tr><tr><td><code>joinType</code></td><td><code>enum</code></td><td><code>fullOuter</code> (default) combines <code>sourceAddress</code> / <code>destinationAddress</code> with <strong>OR</strong>; <code>inner</code> uses <strong>AND</strong>. Only meaningful when both address filters are set.</td></tr><tr><td><code>status</code></td><td><code>enum</code> or <code>enum[]</code></td><td>Any of <code>PENDING</code>, <code>SUBMITTED</code>, <code>OPEN</code>, <code>PARTIAL</code>, <code>FILLED</code>, <code>CANCELLED</code>, <code>EXPIRED</code>, <code>FAILED</code> — see the <a href="#order-status-lifecycle">order status lifecycle</a>. Accepts a single value (<code>?status=FILLED</code>), a repeated key (<code>?status=PENDING&#x26;status=FILLED</code>), or a comma-separated list (<code>?status=PENDING,FILLED</code>).</td></tr><tr><td><code>cursor</code></td><td><code>ISO 8601 string</code></td><td>Opaque pagination cursor — echo back the <code>nextCursor</code> from the previous page. Don't parse or manufacture values; the server compares strictly (<code>createdAt &#x3C; cursor</code>).</td></tr><tr><td><code>limit</code></td><td><code>int</code> (1–100)</td><td>Default 50. Values outside the range return 400.</td></tr></tbody></table>

There is no provider filter — filter by `sourceChain` / `destinationChain` instead.

### List response schema

| Field        | Type                | Description                                                                                                                                     |
| ------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `orders`     | `Order[]`           | Orders owned by the API key, sorted by `createdAt` descending. `Order` schema is [explained above](#order-object).                              |
| `nextCursor` | `string` (optional) | Pagination cursor for the next page. Present **iff** the page is full (`orders.length === limit`); its absence means the last page was reached. |

```json
{
  "orders":     [],
  "nextCursor": "2026-04-23T23:01:32.952Z"
}
```

To iterate: start with no cursor, then keep passing `nextCursor` until the response omits it.

{% hint style="warning" %}
**Terminal states are opt-in.** Omitting `status` does not return *all* statuses. The default is `[PENDING, SUBMITTED, OPEN, PARTIAL]` — active orders only. If you need terminal orders (`FILLED` / `CANCELLED` / `EXPIRED` / `FAILED`), list them explicitly, e.g. `?status=PENDING,SUBMITTED,OPEN,PARTIAL,FILLED,CANCELLED,EXPIRED,FAILED`.
{% endhint %}

{% hint style="warning" %}
**`sourceAddress` + `destinationAddress` are OR by default.** When both are passed, the default join is `fullOuter` (OR) — orders matching *either* side come back. If you need only orders matching *both* addresses, pass `joinType=inner` explicitly. Other filters (`sourceChain`, `destinationChain`, `status`, …) are still AND-combined with whatever the address join produces.
{% endhint %}

### Examples

{% tabs %}
{% tab title="cURL" %}

```bash
# Default — active orders only (PENDING / SUBMITTED / OPEN / PARTIAL)
curl 'https://api.swapkit.dev/v3/limit/orders?limit=50' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'
# → { "orders": [ …50 rows… ], "nextCursor": "2026-04-20T12:34:56.789Z" }

# Next page
curl 'https://api.swapkit.dev/v3/limit/orders?limit=50&cursor=2026-04-20T12:34:56.789Z' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'
# → { "orders": [ …23 rows… ] }   # no nextCursor → end of stream

# Filtered: filled orders from a specific maker address
curl 'https://api.swapkit.dev/v3/limit/orders?sourceAddress=bc1q…&status=FILLED&limit=20' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'

# Filter by chain — BTC → ETH orders
curl 'https://api.swapkit.dev/v3/limit/orders?sourceChain=bitcoin&destinationChain=1' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'

# Address join — fullOuter (default, OR): maker = bc1q… OR receiver = 0xabc…
curl 'https://api.swapkit.dev/v3/limit/orders?sourceAddress=bc1q…&destinationAddress=0xabc…' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'

# Address join — inner (AND): maker = bc1q… AND receiver = 0xabc…
curl 'https://api.swapkit.dev/v3/limit/orders?sourceAddress=bc1q…&destinationAddress=0xabc…&joinType=inner' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'

# Multi-status — repeat the key or use the comma form
curl 'https://api.swapkit.dev/v3/limit/orders?status=PENDING&status=FILLED' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'
curl 'https://api.swapkit.dev/v3/limit/orders?status=PENDING,FILLED' \
  -H 'x-api-key: YOUR_VARIABLE_HERE'
```

{% endtab %}
{% endtabs %}

### Errors

| Status | Error code         | Scenario                                                                                                                                                                        |
| ------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `validation_error` | Bad `limit` (`<1` or `>100`), unparseable `cursor`, or an invalid value in `sourceChain` / `destinationChain` / `status` / `joinType`. The body lists the accepted enum values. |
| 401    | `apiKeyInvalid`    | Missing or unrecognized `x-api-key` header.                                                                                                                                     |

***

## 6. Cancel an order

**Method:** `POST`\
**URL:** `https://api.swapkit.dev/v3/limit/cancel/build`

Force-syncs the order with the provider and returns the appropriate signing artifact for cancellation. One request shape; the artifact you get back depends on how the order's venue and sell chain expect a cancellation to be authorised.

### Request schema

| Parameter | Type     | Required | Description                                                                                     |
| --------- | -------- | -------- | ----------------------------------------------------------------------------------------------- |
| `orderId` | `string` | Yes      | The ID of the order to cancel. Obtained from `/v3/limit/build` or a `/v3/limit/orders` response |

### Cancel build response schema

Exactly one of the three artifacts is non-null. Branch on which field is populated, not on the provider.

| Field                  | Type                | Description                                                                                                                            |
| ---------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `orderId`              | `string`            | UUID of the order being cancelled.                                                                                                     |
| `cancelTx`             | `object` (optional) | Ready-to-broadcast cancel transaction, for venues that cancel on-chain. `null` otherwise. **1inch today.**                             |
| `typedData`            | `object` (optional) | Off-chain cancel payload for the wallet to sign, for venues that accept a signed cancellation. `null` otherwise. **Harbor EVM today.** |
| `btcSignaturePreimage` | `string` (optional) | Canonical-JSON string to sign as a Bitcoin Signed Message, for Bitcoin-side orders. `null` otherwise. **Harbor BTC today.**            |
| `nextActions`          | `object[]`          | Data needed for the next request in the flow (`/v3/limit/cancel/submit` call).                                                         |

```json
{
  "orderId": "ord_…",
  "cancelTx": null,
  "typedData": null,
  "btcSignaturePreimage": null,
  "nextActions": []
}
```

{% tabs %}
{% tab title="cancelTx — broadcast on-chain" %}
The wallet broadcasts a cancel transaction to the venue's contract. Any existing token allowance is unaffected — only the specific order is invalidated. On 1inch this is a `cancelOrder(makerTraits, orderHash)` call on the aggregation router.

```json
"cancelTx": {
  "to": "0x…",
  "data": "0x…",
  "value": "0",
  "chainId": 1
}
```

The wallet broadcasts it; you then post the resulting `txHash` to `/v3/limit/cancel/submit`.
{% endtab %}

{% tab title="typedData — sign off-chain" %}
The wallet signs an EIP-712 cancellation message. Nothing goes on-chain from your side. Harbor's EVM payload uses `CancelAndWithdraw`:

```json
"typedData": {
  "primaryType": "CancelAndWithdraw",
  "types": {
    "CancelAndWithdraw": [
      { "name": "action",    "type": "string"  },
      { "name": "chain",     "type": "string"  },
      { "name": "l1Address", "type": "address" },
      { "name": "nonce",     "type": "uint256" },
      { "name": "expiry",    "type": "uint256" },
      { "name": "payload",   "type": "CancelAndWithdrawPayload" }
    ],
    "CancelAndWithdrawPayload": [
      { "name": "orderId", "type": "string" }
    ]
  },
  "domain": {},
  "message": {
    "action":    "cancel_and_withdraw",
    "chain":     "ETH",
    "l1Address": "0x…",
    "nonce":     1714500000,
    "expiry":    1714500600,
    "payload":   { "orderId": "trd-<uuid>" }
  }
}
```

Sign the payload exactly as returned. Note that `payload.orderId` is the **provider's** internal order id, *not* the SwapKit `orderId` or `orderHash` — on Harbor it's the `clientOrderId` (`trd-…`). `nonce` and `expiry` are JSON numbers (uint256 wire-encoded as JS numbers; values stay below `Number.MAX_SAFE_INTEGER`).
{% endtab %}

{% tab title="btcSignaturePreimage — Bitcoin Signed Message" %}
The wallet signs the canonical-JSON string as a Bitcoin Signed Message (BIP-137).

```json
"btcSignaturePreimage": "{\"action\":\"cancel_and_withdraw\",\"chain\":\"BTC\",\"domain\":\"harbor.orderbook.trading\",\"expiry\":1714500600,\"l1_address\":\"bc1q…\",\"nonce\":1714500000,\"payload\":{\"orderId\":\"trd-<uuid>\"}}"
```

Submit the base64-encoded compact secp256k1 signature.
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**Sign the preimage byte-for-byte.** The `btcSignaturePreimage` string is canonical JSON — keys **alphabetically sorted**, **no whitespace**, applied **recursively** — and the signature is verified over exactly those bytes. Sign the string as returned rather than re-serialising the object.

Two field-naming traps if you do rebuild it: the Bitcoin payload uses **snake\_case** `l1_address` where the EVM payload uses camelCase `l1Address`, and its `domain` is a **flat string** (`"harbor.orderbook.trading"`) rather than the EVM nested `{ name, version, … }` object.
{% endhint %}

{% hint style="warning" %}
**Preconditions.** `/v3/limit/cancel/build` force-syncs the order with the provider before responding. If the provider hasn't acknowledged the order yet, the call fails with `limitOrderInvalidState` — on Harbor, with the message *"Harbor clientOrderId not yet known — wait until status reaches OPEN"*. Wait for the order to advance past `SUBMITTED` before attempting cancellation.
{% endhint %}

***

## 7. Submit the cancellation

**Method:** `POST`\
**URL:** `https://api.swapkit.dev/v3/limit/cancel/submit`

Records the broadcast cancel transaction or the wallet's cancellation signature, depending on which artifact `/v3/limit/cancel/build` returned.

### Request schema

| Parameter   | Type     | Required for                        | Description                                                                                                                                  |
| ----------- | -------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `orderId`   | `string` | All cancellations                   | The ID of the order being cancelled. Obtained from a previous `/v3/limit/cancel/build` response                                              |
| `txHash`    | `string` | `cancelTx`                          | Hash of the broadcast cancel transaction.                                                                                                    |
| `signature` | `string` | `typedData`, `btcSignaturePreimage` | Wallet's cancellation signature — `0x`-hex for an EIP-712 signature, base64 for a BIP-137 one. The provider verifies the format server-side. |

```json
// Broadcast cancel tx
{ "orderId": "ord_…", "txHash": "0x…" }

// EIP-712 signature (0x-hex)
{ "orderId": "ord_…", "signature": "0x…" }

// BIP-137 signature (base64)
{ "orderId": "ord_…", "signature": "H4sIAAA…" }
```

### Cancel submit response schema

| Field     | Type                | Description                                                                                              |
| --------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
| `orderId` | `string`            | UUID of the cancelled order.                                                                             |
| `status`  | `enum`              | `CANCELLED` on success. See the [order status lifecycle](#order-status-lifecycle).                       |
| `txHash`  | `string` (optional) | Hash of the broadcast cancel transaction. Returned only for the `cancelTx` path; omitted for signatures. |

```json
{
  "orderId": "ord_…",
  "status":  "CANCELLED",
  "txHash":  "0x…"
}
```

{% hint style="warning" %}
**Cancel payloads can expire.** Signed cancel payloads carry a server-side `expiry` — Harbor's is set **10 minutes** after `/v3/limit/cancel/build`. If the wallet takes longer than that to sign and you submit a stale payload, `/v3/limit/cancel/submit` returns `limitOrderInvalidState` with a message such as *"cancel payload expired — call /v3/limit/cancel/build again"*. Read `expiry` off the payload rather than hard-coding the window, and have hardware-wallet flows re-build before signing if they're running close to it.
{% endhint %}

### Errors

| Status | Error code               | Scenario                                                                                                                                                                                                               |
| ------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 409    | `limitOrderInvalidState` | The order is in a state that doesn't allow cancellation: a terminal state, the provider hasn't acknowledged it yet, the payload is past its expiry, or it's already cancelled. The body carries a descriptive message. |
| 400    | `limitOrderCancelFailed` | The provider rejected the cancellation.                                                                                                                                                                                |
| 404    | `limitOrderNotFound`     | Unknown `orderId`, or it belongs to a different API key.                                                                                                                                                               |

***

## Order status lifecycle

`PENDING` → `SUBMITTED` → `OPEN` → `PARTIAL` → `FILLED` / `CANCELLED` / `EXPIRED` / `FAILED`

Everything past `SUBMITTED` is driven by a poller that reconciles `SUBMITTED` / `OPEN` / `PARTIAL` orders against the provider every 30 minutes, publishing each transition to the [webhook deliverer](#webhooks). `GET /v3/limit/orders/:orderId` force-syncs on every call — use it when you need state fresher than the poller interval.

| Status                                     | Description                                                                                                                                                                                                        |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PENDING`                                  | The order has been built but `/v3/limit/submit` hasn't been called yet. No signature or deposit hash on file.                                                                                                      |
| `SUBMITTED`                                | `/v3/limit/submit` has attached the wallet signature or the deposit tx hash, but the provider hasn't acknowledged the order as resting yet (still in the mempool, or not yet indexed).                             |
| `OPEN`                                     | Emitted by the poller once the provider confirms the order is resting. **Not monotonic** — if a provider temporarily has no record of an order, sync can regress it `OPEN` → `SUBMITTED` until visibility returns. |
| `PARTIAL`                                  | The order has been partly filled. Only emitted by providers that support partial fills; all-or-nothing paths go straight to `FILLED`.                                                                              |
| `FILLED`, `CANCELLED`, `EXPIRED`, `FAILED` | Terminal.                                                                                                                                                                                                          |

{% hint style="warning" %}
**Auto-expiry of unsubmitted orders.** At the start of every poller tick, SwapKit sweeps `PENDING` orders that never received a `/v3/limit/submit` (no signature, no deposit hash) and flips them to `EXPIRED` when either the order's own `expiresAt` has passed *or* it has been sitting unsubmitted for longer than the grace window (**1 hour by default**, configurable via `LIMIT_ORDER_SUBMIT_GRACE_SECONDS`). The sweep is provider-agnostic. Orders are marked `EXPIRED` rather than deleted so they remain in the audit history. Attempts to submit an already-expired order return `limitOrderInvalidState`.
{% endhint %}

***

## Webhooks

Configure `apiKey.settings.LIMIT_ORDER_WEBHOOK_URL` (under `ApiKeySettingsSchema`, alongside `NOTIFICATION` and `VAULT_SWAPS`; `apiKey.config` is reserved strictly for fee configuration). The webhook deliverer POSTs every status transition over HTTP, for orders on any provider.

Verify authenticity via the `x-swapkit-signature` header, which carries an HMAC-SHA256 of the raw body keyed to the webhook secret.

### Headers

| Header                       | Description                                                   |
| ---------------------------- | ------------------------------------------------------------- |
| `x-swapkit-signature`        | Hex HMAC-SHA256 of the raw body, keyed to the webhook secret. |
| `content-type`               | Always `application/json`.                                    |
| `x-swapkit-event-id`         | Matches `eventId` in the body. Dedupe on it.                  |
| `x-swapkit-delivery-attempt` | 1-based delivery attempt number for this event.               |

### Body schema

<table><thead><tr><th width="239.48046875">Field</th><th width="178.68359375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>type</code></td><td><code>string</code></td><td>Always <code>"limitOrder.statusChanged"</code>.</td></tr><tr><td><code>eventId</code></td><td><code>string</code></td><td>Unique id for this delivery event. Use it to dedupe redeliveries.</td></tr><tr><td><code>orderId</code></td><td><code>string</code></td><td>UUID of this specific order.</td></tr><tr><td><code>orderHash</code></td><td><code>string</code></td><td>Canonical on-chain identifier of the order, <code>0x</code>-prefixed.</td></tr><tr><td><code>apiKeyId</code></td><td><code>number</code></td><td>Numeric id of the API key that owns the order.</td></tr><tr><td><code>provider</code></td><td><code>string</code></td><td>Provider that routed the order.</td></tr><tr><td><code>chainId</code></td><td><code>string</code></td><td>ChainId of the sell asset.</td></tr><tr><td><code>maker</code></td><td><code>string</code></td><td>Sell-side address that owns the order.</td></tr><tr><td><code>sellAsset</code>, <code>buyAsset</code></td><td><code>string</code></td><td>SwapKit asset identifiers.</td></tr><tr><td><code>sellAmount</code>, <code>buyAmount</code></td><td><code>decimal string</code></td><td>Order amounts, human-readable decimal.</td></tr><tr><td><code>limitPrice</code></td><td><code>decimal string</code></td><td><code>buyAsset</code> per 1 <code>sellAsset</code></td></tr><tr><td><code>previousStatus</code></td><td><code>enum</code></td><td>The status the order transitioned from.</td></tr><tr><td><code>status</code></td><td><code>enum</code></td><td>The new status. See the <a href="#order-status-lifecycle">order status lifecycle</a>.</td></tr><tr><td><code>previousFilledSellAmount</code>, <code>previousFilledBuyAmount</code></td><td><code>decimal string</code></td><td>Filled amounts before this transition.</td></tr><tr><td><code>filledSellAmount</code>, <code>filledBuyAmount</code></td><td><code>decimal string</code></td><td>Filled amounts after this transition.</td></tr><tr><td><code>integratorFeeBps</code></td><td><code>number</code> or <code>null</code></td><td>Integrator fee encoded on the order.</td></tr><tr><td><code>createdAt</code></td><td><code>ISO 8601 string</code></td><td>When the transition was recorded.</td></tr></tbody></table>

***

## Integrator checklist

1. **Obtain an API key configured for limit orders.** The affiliate fee and integrator recipient live on the key's config — see [Monetization](/monetization.md).
2. **Call** [**`/v3/limit/quote`**](#id-1.-price-the-pair)**.** Pass the pair plus **any two** of `sellAmount` / `buyAmount` / `limitPrice`. Optionally pre-send `sourceAddress` / `destinationAddress` for early screening.
3. **Inspect the route.** `spotPriceDeviationBps` shows the user how far their limit is from spot; `minExpirationSeconds` / `maxExpirationSeconds` bound the `expiresAt` you can pass to `/v3/limit/build`.
4. **Call** [**`/v3/limit/build`**](#id-2.-build-the-signing-artifact) with `routeId`, `sourceAddress`, `destinationAddress`, and `expiresAt`.
5. **Handle the token approval.** Check [`isApproved`](#the-isapproved-field) and broadcast the accompanying [`approvalTx`](#the-approvaltx-object) first if it's `false`.
6. **Sign per the** [**signing model**](#signing-models)**.** If `typedData` is non-null, the wallet signs it and you post `{ orderId, signature }` to [`/v3/limit/submit`](#id-3.-submit-the-signed-artifact). If `tx` is non-null, the wallet signs and broadcasts it — using `txMeta.txType` to pick the right codec — and you post `{ orderId, depositTxHash }`.
7. **Track state.** Poll [`GET /v3/limit/orders/:orderId`](#id-4.-single-order-detail) (which also refreshes from the provider) or paginate via [`GET /v3/limit/orders`](#id-5.-paginated-order-list). Treat `OPEN` as [non-monotonic](#order-status-lifecycle).
8. **Cancel when needed.** [`POST /v3/limit/cancel/build`](#id-6.-cancel-an-order) → the wallet signs or broadcasts whichever artifact came back → [`POST /v3/limit/cancel/submit`](#id-7.-submit-the-cancellation).
9. **Optional: webhooks.** Configure `apiKey.settings.LIMIT_ORDER_WEBHOOK_URL` to receive `PENDING` → `SUBMITTED` → `OPEN` → `PARTIAL` → `FILLED` / `CANCELLED` / `EXPIRED` transitions over HTTP.

{% hint style="info" %}
**Staying forward-compatible.** Branch on the artifact (`typedData !== null`), never on `provider`. Switch on `txMeta.txType` rather than on the chain, so an unrecognised value fails loudly instead of being signed with the wrong codec. Treat `provider`, `warnings[].code`, `txMeta.txType`, and `fees[].type` as open enums with an explicit fallback — new values ship with new providers and chains. And read `minExpirationSeconds` / `maxExpirationSeconds` and `fees[]` off each quote rather than hard-coding today's values. An integration written that way picks up new providers without a code change.
{% endhint %}
