> 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

Offer your users price-contingent swaps that execute asynchronously when the market reaches their target.

{% 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.
{% 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><tr><td><code>providers</code></td><td><code>enum[]</code></td><td>No</td><td>Restrict routing to these providers. Omit to consider every provider that supports the pair.</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.

`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.                                                   |
| 400    | `limitOrderAmountAmbiguous`   | Not exactly two of `sellAmount` / `buyAmount` / `limitPrice` were supplied.                                                                                          |
| 400    | `limitOrderUnsupportedPair`   | No limit-order provider implements this pair at all.                                                                                                                 |
| 400    | `limitOrderUnsupportedChain`  | Limit orders aren't offered on that chain.                                                                                                                           |
| 400    | `limitOrderChainMismatch`     | The pair spans two chains on a path that requires both assets on one.                                                                                                |
| 401    | `apiKeyInvalid`               | Missing or unrecognized `x-api-key` header.                                                                                                                          |
| 502    | `limitOrderProviderError`     | The upstream provider rate-limited, returned a 5xx, or failed in transport while pricing. Deliberately generic. **Worth retrying.**                                  |
| 503    | `limitOrderActionUnavailable` | A provider supports the pair, but the action is currently disabled for one of its chains. Ops-controlled, so it can clear without a change on your side.             |

***

## 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>number</code> (optional)</td><td>EVM chain the deposit is broadcast on, as a <strong>number</strong> (<code>1</code>, <code>42161</code>) — not a string. Present only when <code>txMeta.txType</code> is <code>"evm"</code>; <strong>omitted entirely on the <code>psbt</code> path</strong>, where the chain is implied by the PSBT. Read the chain from <code>txType</code> rather than expecting this field.</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>Nothing extra to broadcast — though not a guarantee that the allowance was verified. <a href="#how-the-allowance-check-is-evaluated">See below</a>.</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 %}

#### How the allowance check is evaluated

`isApproved` is not a general "is this token approved" flag. It reports whether **this order** can be pulled, on the same amount-scoped and spender-scoped terms `/v3/swap` uses — see [How the allowance is read](/swapkit-api/v3-swap-obtain-swap-transaction-details.md#how-the-allowance-is-read) for the mechanics and the staleness caveat.

So a token the wallet already shows as approved can still come back `false`: approving 1 USDT and then building a 2 USDT order returns `false`, and an allowance granted for an earlier order on a different provider does not carry over, because each provider pulls through its own contract.

The check is advisory as well — if the read fails outright, the build returns `isApproved: true` rather than failing with it, so `true` is not proof the allowance exists.

### Errors

| Status | Error code                        | Scenario                                                                          |
| ------ | --------------------------------- | --------------------------------------------------------------------------------- |
| 404    | `limitOrderQuoteNotFound`         | The `quoteId` is unknown or has expired. Re-price with `/v3/limit/quote`.         |
| 404    | `limitOrderRouteNotFound`         | The `routeId` isn't one of the routes that quote returned.                        |
| 400    | `limitOrderExpirationOutOfBounds` | `expiresAt` sits outside the quote's expiration bounds. The message carries both. |
| 500    | `limitOrderBuildFailed`           | The provider accepted the pair but failed to build the order.                     |

***

## 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 %}

#### Submit errors

The orderbook rejects most bad submissions for a **client-side** reason. Those are reported as 4xx and are not worth retrying — only a genuine upstream fault is a `502`.

| Status | Error code                        | Scenario                                                                                                                                                                                         |
| ------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 451    | `limitOrderMakerBlacklisted`      | The orderbook refuses orders from this maker address — a compliance decision made upstream, not by SwapKit's screening. Permanent for the address; the order can never be submitted by retrying. |
| 400    | `limitOrderInsufficientAllowance` | The maker hasn't approved enough of the sell token for the limit-order router. **Recoverable:** re-run `/v3/limit/build` and broadcast the returned `approvalTx`, then submit again.             |
| 400    | `limitOrderMaxOrdersExceeded`     | The maker already holds the orderbook's maximum of simultaneously valid orders (1inch: 100). **Recoverable:** cancel an existing order first. The message carries the cap.                       |
| 409    | `limitOrderInvalidState`          | The order is already in the book, or has advanced past `PENDING`. Same code as the re-submit guard above.                                                                                        |
| 400    | `limitOrderRejected`              | Any other client-side rejection SwapKit doesn't model individually — bad signature, expired, insufficient balance. Carries the orderbook's own description.                                      |
| 502    | `limitOrderSubmissionFailed`      | A genuine upstream fault: the orderbook returned a 5xx, or the call failed in transport with no status. **This one is worth retrying.**                                                          |

***

## 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. 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>.<br><br>Matches only orders whose destination chain was recorded when the order was created. Orders predating that column aren't backfilled, so an older order won't match this filter even when its buy asset is on the chain you asked for.</td></tr><tr><td><code>joinType</code></td><td><code>enum</code></td><td>How each filter <strong>pair</strong> combines internally — <code>fullOuter</code> (default) is <strong>OR</strong>, <code>inner</code> is <strong>AND</strong>. Applies to the address pair and the chain pair alike, and only matters when both halves of a pair 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" %}
**The chain pair is OR by default too**, which is the surprising half: `?sourceChain=1&destinationChain=bitcoin` returns every order touching Ethereum **or** Bitcoin on either side — *not* Ethereum→Bitcoin orders. Pass `joinType=inner` to require both halves.

The two axes are then AND'd with each other, and `status` on top of both.
{% 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.** Switch on `txMeta.txType` rather than on the chain, so an unrecognised value fails loudly instead of being signed with the wrong codec — and treat it, `provider`, `warnings[].code` and `fees[].type` as open enums with an explicit fallback. Read `minExpirationSeconds` / `maxExpirationSeconds` and `fees[]` off each quote rather than hard-coding today's values.
{% endhint %}
