# Float partner API

Float swaps **Canton Coin (CC)** to and from about 2,000 assets, at the best rate across Float's liquidity partners. This API lets your app do it for your users: quote a swap, open an order, show your user where to send, and follow the order until it is done. You earn **0.2% of the CC side of every order** you route.

- **Base URL:** `https://api.myfloat.cc/v1`
- **OpenAPI 3.1:** [https://www.myfloat.cc/openapi.json](https://www.myfloat.cc/openapi.json) (also served by the API at `/v1/openapi.json`)
- **This page as Markdown:** [https://www.myfloat.cc/docs/api.md](https://www.myfloat.cc/docs/api.md)
- **Build it with an AI assistant:** [https://www.myfloat.cc/docs/ai](https://www.myfloat.cc/docs/ai)
- **Support:** support@myfloat.cc

> The document at the API host's root, `/openapi.json`, is a different product (Slay's wallet API) — **Float's spec is `/v1/openapi.json`**.

## 1. Overview

Two directions, one API:

| Direction | Your user | `direction` |
|---|---|---|
| **Get CC** | sends an asset (USDT, BTC, ETH, …) and receives CC in their Canton wallet | `in` |
| **Withdraw CC** | sends CC and receives an asset at an address on its chain | `out` |

Opening an order **moves no money**. It returns a deposit address and an exact amount; your user sends from their own wallet, and an order nobody pays simply expires.

**There is no sandbox.** `GET /v1/assets` and `POST /v1/quote` are always safe to call. `POST /v1/order` opens a real order at a swap partner — harmless until someone pays it, but do not send a deposit while testing unless you mean to swap, and do not open orders in automated tests.

### Integration flow in five steps

1. **List currencies** — `GET /v1/assets?all=1` once, for your token picker (logos included).
2. **Estimate** — `POST /v1/quote` as the user types; show `amountOut` and the routes.
3. **Create the exchange** — `POST /v1/order` from your server, with your API key and an `Idempotency-Key`.
4. **Show the deposit** — `depositAddress`, the exact `amountIn`, the network and a countdown to `depositDeadline`.
5. **Track it** — receive `order.state_changed` [webhooks](#6b-webhooks), and/or poll `GET /v1/order/{claimToken}` every 10–30 seconds, until `state` is final.

```text
 User            Your app (browser)        Your server               Float API /v1          Swap partner
  |  pick token, amount  |                       |                          |                     |
  |--------------------->|  /api/float/quote --->|  POST /quote (key) ----->|---- asks every ---->|
  |                      |<---- best + routes ---|<-------------------------|<-------------------- |
  |  address, "Exchange" |                       |                          |                     |
  |--------------------->|  /api/float/order --->|  POST /order (key,       |                     |
  |                      |                       |   Idempotency-Key) ----->|---- opens order --->|
  |                      |<-- depositAddress, amountIn, depositDeadline ----|                     |
  |  sends exact amountIn from own wallet ------------------------------------> Float (CC→X) or partner (X→CC)
  |                      |  every 10-30 s ------>|  GET /order/{token} ---->|                     |
  |<---- "Done" ---------|<-- state: completed --|<-------------------------|                     |
```

```mermaid
sequenceDiagram
  participant U as User
  participant A as Your app
  participant S as Your server (holds the key)
  participant F as Float /v1
  U->>A: token, amount
  A->>S: quote
  S->>F: POST /quote
  F-->>S: best route + alternatives
  U->>A: address, Exchange
  A->>S: create order
  S->>F: POST /order (Bearer key, Idempotency-Key)
  F-->>S: depositAddress, amountIn, depositDeadline, claimToken
  U->>F: sends exactly amountIn (CC→X) / to the partner (X→CC)
  loop every 10–30 s until final
    S->>F: GET /order/{claimToken}
  end
```

### Endpoints

| Method | Path | What it does |
|---|---|---|
| `GET` | `/v1/assets` | List or search the assets Float can swap (`?all=1` for a picker) |
| `POST` | `/v1/quote` | Indicative price across every partner |
| `POST` | `/v1/order` | Create an order; returns where the user sends |
| `GET` | `/v1/order/{claimToken}` | Read an order (status) |
| `POST` | `/v1/order/{claimToken}/refund-address` | Add the user's refund address (X → CC) or refund party (CC → X) |
| `GET` | `/v1/r/{code}` | Resolve a referral code (counts a click) |
| `GET` `POST` `PATCH` `DELETE` | `/v1/webhooks…` | Register and manage webhook endpoints (API key required) — see [Webhooks](#6b-webhooks) |
| `GET` | `/v1/openapi.json` | This API as OpenAPI 3.1 |

Every request and response is JSON. Any other path under `/v1` is `404 not_found`.

## 2. Authentication

`Authorization: Bearer fl_live_…` — optional on every endpoint, required in practice for orders you want to earn on.

**Create a key:** sign in at [https://www.myfloat.cc/partners](https://www.myfloat.cc/partners) → **API for developers** → **Create new API key**. The secret is shown **once** (only a hash is stored). You can restrict a key to up to 32 IP addresses, create as many as you like, and revoke any of them.

| With a key | Without a key |
|---|---|
| Orders are attributed to you (0.2%) | Unattributed, unless the order carries a referral `ref` |
| Rate-limited **per key** | Rate-limited per IP: 12 requests a minute |
| `Idempotency-Key` accepted on `POST /order` | `Idempotency-Key` refused (`400 idempotency_requires_key`) |

A key that is present but unknown, revoked or used from an IP it does not allow is `401 invalid_key` — never silently treated as anonymous.

### Server-side only

- **Keys are server secrets.** Keep the key in a server-side environment variable (for example `FLOAT_API_KEY`). Never put it in a web page, a mobile app bundle, a URL, or your repository. It is only ever read from the `Authorization` header.
- **Call the API from your backend.** The API is built for server-to-server use. Browser calls from your own domain are **not supported**: CORS does not allow them, so the browser blocks your page from reading the answer. Put a small proxy route on your server (the [starter](https://www.myfloat.cc/docs/ai) has one) and have your front end call that. `/v1` answers carry no `Access-Control-Allow-Origin` or `Access-Control-Allow-Credentials` header; the one exception is `GET /v1/openapi.json`, which any origin may read (`Access-Control-Allow-Origin: *`).
- **No-code alternative:** the [embed widget](#10-embed-widget) runs on Float's domain inside an iframe and is attributed with a referral code, which is safe to publish.

### Rate limits

| Caller | Limit |
|---|---|
| With an API key | Per key, per minute: **60** `POST /quote`, **20** `POST /order` (and `/refund-address`), **300** other requests. Counted across all of Float's servers. |
| Without a key | **12 requests a minute per IP**, approximate (counted per edge instance). A server calling without a key shares that one budget across **all** of its users — use a key. |

Keyed responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (unix seconds the window ends); keyless ones carry the first two. Over the limit: `429 rate_limited` with `Retry-After` in seconds — wait, then retry the identical request. Windows are fixed minutes. `/v1/openapi.json` is not limited. `/v1/webhooks` calls are limited to 30 a minute per IP (test events: 6 a minute). Need more? Ask support — every quote fans out to every partner.

### Idempotency-Key

A timeout on `POST /order` does not tell you whether an order opened. Send an `Idempotency-Key` header — a UUID per **intended** order — and retry with the identical body:

| Retry with | Result |
|---|---|
| same key, same body | **the same order**, `201`, header `Idempotent-Replayed: true` — no second order |
| same key, different body | `409 idempotency_key_reused` |
| same key while the first is still opening | `409 idempotency_in_progress` with `Retry-After: 2` — retry the same request |

Keys need an API key, are remembered **per partner** (all your keys share one namespace) for **24 hours**, and are 1–255 visible ASCII characters (`400 idempotency_key_invalid` otherwise). "Same body" means the same JSON values (key order and whitespace do not matter; `"0.01"` and `0.01` differ). A request that fails frees its key. A replayed order is read fresh, so its `state` is current.

## 3. Currencies — `GET /v1/assets`

Assets at least one partner will swap into CC, best-covered first.

| Query | Default | |
|---|---|---|
| `q` | — | Case-insensitive match on ticker or name. Exact ticker first, then prefix, then anything. |
| `limit` | 50 | Clamped to 1–200. |
| `offset` | 0 | Page past `limit`. `total` is the number of matching rows. |
| `network` | — | Only rows on this chain, any spelling (`BSC`, `BNB`, `bep20`); `none` = rows with no known chain. |
| `all` | — | `all=1` returns **every** row in compact form (cached 10 minutes); other parameters are ignored. Use this for a token picker. |

```bash
curl "https://api.myfloat.cc/v1/assets?q=USDT&limit=2"
```

```json
{
  "landingAsset": "CC",
  "total": 18,
  "offset": 0,
  "assets": [
    { "symbol": "USDT", "venues": ["houdiniswap", "rocketx", "letsexchange", "exolix", "thechange"], "name": "Tether",
      "network": "TRON", "networkName": "Tron",
      "icon": "https://www.myfloat.cc/icons/tokens/usdt.webp", "networkIcon": "https://www.myfloat.cc/icons/chains/tron.png" },
    { "symbol": "USDT-APT", "venues": ["letsexchange"], "name": "TetherUS",
      "network": "APTOS", "networkName": "Aptos",
      "icon": "https://www.myfloat.cc/icons/tokens/usdt.webp", "networkIcon": "https://www.myfloat.cc/icons/chains/aptos.png" }
  ]
}
```

`?all=1` rows carry only `symbol`, `name`, `network`, `networkName`, `icon`, `networkIcon` and, where they apply, `destinationExtraIdNeeded` / `destinationExtraIdName`.

| Field | |
|---|---|
| `symbol` | The ticker to send as `from` (Get CC) or `to` (Withdraw CC). Some are chain-suffixed (`USDT-APT`, `USDC-SOL`), but not consistently (`USDT` on Tron has no suffix, and a token can have both a plain and a suffixed row). **Identify an asset by the pair `symbol` + `network`**, never by parsing a suffix. |
| `name` | Display name. May be absent. |
| `network` | Float's code for the row's chain (`TRON`, `ETHEREUM`, `BSC`, …). Absent when unknown. Send it as `network` on `/quote` and `/order` (omit the key when the row has none). |
| `networkName` | Display name of the chain, or null. |
| `icon` | Float-hosted 64×64 WebP token image (absolute URL): the token's own, else its base token's (`USDT-APT` → USDT), else the chain logo, else **null — draw a lettered tile**. |
| `networkIcon` | Float-hosted 64×64 chain logo (PNG or WebP), or null. Use it as the badge on the token logo and on network chips. |
| `destinationExtraIdNeeded`, `destinationExtraIdName` | Only on rows whose payouts take a destination tag / memo: `true`, and what to call it (`"Destination tag"`, `"Memo"`, `"Memo (comment)"`). Show the field on Withdraw CC. This flag (and the same one on the quote) is authoritative. |
| `venues` | Partner ids that list it. Informational. |

**Limits (min / max)** depend on the route and the size, so they come from `POST /v1/quote` (`minIn`, `maxIn`, or the `409` message), not from this list.

**Each asset is listed once.** A chain-suffixed ticker that is only another spelling of a pinned asset on the same chain (`USDT-TRC20` beside `USDT` on Tron, `USDC-ERC20` beside `USDC` on Ethereum, `VELO-XLM` beside `VELO` on Stellar…) is left out of `/v1/assets`. Those tickers are still accepted on `/quote` and `/order` and quote exactly like the plain one.

**Destination tag / memo.** Rows whose payouts take a tag or memo (an exchange's shared XRP, XLM, TON, ATOM, EOS, HBAR… deposit address) carry `destinationExtraIdNeeded: true` and `destinationExtraIdName` (`"Destination tag"` for XRP, `"Memo (comment)"` on TON, else `"Memo"`); both are absent on every other row. **The flag is authoritative** — read it from the row rather than keeping your own list; the list below is a snapshot. On 20 Oct 2026 (`GET /v1/assets?all=1`): XRP, XLM, HBAR, ATOM, EOS, INJ, KAVA, OSMO, TIA, RUNE, LUNA, SEI, DYDX, STX, ALEX, ALEX-STX, AKT, SCRT, XPR, POLYX, NIBI, ORAI, PI, VELO and the TON tokens (GRAM, GRAM-TON, NOT, DOGS, HMSTR, MAJOR, MAJOR-TON, REDO, JUSDT, USDT-TON). See [Destination tag / memo](#destination-tag-memo-destinationextraid).

### Networks and pinned chains

Many assets live on several chains. `network` on `/quote` and `/order` names the chain of the **non-CC** asset — what the user sends on (`in`) or is paid on (`out`). CC is always on Canton. Any common spelling is accepted and answered with Float's code:

| Float's code | Also accepted (examples) |
|---|---|
| `BITCOIN` | `BTC` |
| `ETHEREUM` | `ETH`, `ERC20`, `ERC-20` |
| `TRON` | `TRX`, `TRC20` |
| `BSC` | `BEP20`, `BNB`, `BINANCE`, `BNB Smart Chain` |
| `SOLANA` | `SOL`, `SPL` |
| `POLYGON` | `MATIC`, `POL` |
| `ARBITRUM` | `ARB`, `ARBITRUM ONE` |
| `OPTIMISM` | `OP` |
| `BASE` | |
| `AVALANCHE` | `AVAX`, `AVAXC`, `C-CHAIN` |
| `HYPEREVM` | `HYPEEVM`, `HYPE-EVM` — not `HYPERCORE` (Hyperliquid's L1) |
| `TON`, `XRP`, `LITECOIN`, `DOGECOIN`, `ZKSYNC`, `COSMOS` … | `LTC`, `DOGE`, `RIPPLE`, `ZKSYNC-ERA`, `COSMOSHUB-4` … |

There are about 140 chains. Unknown names keep their own spelling, upper-cased.

- **With `network`:** only quotes on that chain are considered. A different chain is **never** substituted (a deposit on the wrong chain is lost). No partner on that chain is `409 conflict`.
- **A chain-suffixed ticker** (`USDC-SOL`, `USDT-TRC20`) is the plain asset on that chain, exactly as if `network` were sent. A `network` that names a different chain is `400 invalid_request`.
- **Without `network`:** an asset Float pins to a chain (below) is quoted on that chain at every partner (`networkChoice: "float_default"`). Any other asset uses each partner's own default chain, **which can differ between partners** (`networkChoice: "venue_default"`). If the chain matters, send `network` — the picker row's `network` is the right value.

| Pinned asset | Chain |
|---|---|
| ETH, USDC, DAI, LINK, UNI, AAVE, WBTC, USDE, PYUSD, TEL, XCN, SPA, VOOI, PROMPT, TOKEN, EDGE, H, SPX, FORT, LADYS, ORDER, SWFTC, CRO, KCS, TLOS | `ETHEREUM` |
| USDT, TRX, USDD | `TRON` |
| BNB, BABYDOGE, USD1 | `BSC` |
| SOL, SOON, ZEUS, ACS, ARX, USDG | `SOLANA` |
| MATIC, TRADE | `POLYGON` |
| BTC, AVAX, LTC, XRP, DOGE, DOT, ADA, ATOM, XMR, BCH, XLM, ETC | their own chain (`BITCOIN`, `AVALANCHE`, `LITECOIN`, `XRP`, `DOGECOIN`, `POLKADOT`, `CARDANO`, `COSMOS`, `MONERO`, `BITCOINCASH`, `STELLAR`, `ETC`) |
| ARB, OP, HYPE, MNT, TAIKO, BRISE, WAVES, ELA, XPR, ORAI, 0G, VELO | `ARBITRUM`, `OPTIMISM`, `HYPEREVM`, `MANTLE`, `TAIKO`, `BITGERT`, `WAVES`, `ELASTOS`, `XPR`, `ORAICHAIN`, `0G`, `STELLAR` |

The order opens on the chain its quote was priced on and records it (`network` on the order). If the partner would open it on any other chain, it is refused with `409 venue_unavailable` before anything is sent.

### Build a token picker

`GET /v1/assets?all=1` is everything a picker needs: about 2,000 rows with Float-hosted token logos and chain logos. Load it once (through your proxy), filter in the browser.

- **Popular first:** USDT, USDC, BTC, ETH, SOL, BNB, XRP, TRX, DOGE, LTC, TON, AVAX — then the rest A–Z.
- **Search** by ticker or name: exact ticker, then prefix, then anything.
- **Network filter** (chips or a column) built from the rows' `network` / `networkName` / `networkIcon`.
- **Token logo with chain badge:** `icon` with `networkIcon` in the corner; a **lettered tile** when either is null or fails to load.
- **Return** `symbol` and `network` and quote with them. Key rows (React keys, favourites, recent picks) on the pair `symbol` + `network`, not on the ticker alone or a suffix like `-SOL`.

```html
<input id="q" placeholder="Search by name or ticker">
<div id="nets"></div>
<ul id="list"></ul>
<script type="module">
  // Your server proxies this to GET /v1/assets?all=1 (never call Float from the browser).
  const { assets } = await (await fetch("/api/float/assets?all=1")).json();
  const POPULAR = ["USDT", "USDC", "BTC", "ETH", "SOL", "BNB", "XRP", "TRX", "DOGE", "LTC", "TON", "AVAX"];
  const rows = assets.filter((a) => a.symbol !== "CC");
  let net = "all";

  // Built with createElement / textContent: names and symbols are data, never HTML.
  const el = (tag, props = {}, ...kids) => {
    const e = Object.assign(document.createElement(tag), props);
    e.append(...kids); // strings become text nodes
    return e;
  };
  const tile = (label) => el("span", { className: "tile", textContent: (label[0] ?? "?").toUpperCase() });
  const img = (src, label, size) => {
    if (!src) return tile(label);
    const i = el("img", { src, width: size, height: size, alt: "" });
    i.onerror = () => i.replaceWith(tile(label));
    return i;
  };

  // Network chips: one per chain, biggest first.
  const nets = new Map();
  for (const a of rows) if (a.network) {
    const n = nets.get(a.network) ?? { ...a, n: 0 };
    n.n++; nets.set(a.network, n);
  }
  const chip = (label, value, icon) =>
    el("button", { onclick: () => { net = value; render(); } }, ...(icon ? [icon, " "] : []), label);
  document.querySelector("#nets").replaceChildren(
    chip("All", "all"),
    ...[...nets.values()].sort((x, y) => y.n - x.n).slice(0, 20)
      .map((n) => chip(n.networkName ?? n.network, n.network, img(n.networkIcon, n.networkName ?? n.network, 16)))
  );

  function pick(a) {
    // What /v1/quote needs: { from (or to): symbol, network }
    console.log("picked", { symbol: a.symbol, network: a.network ?? undefined });
  }

  function render() {
    const q = document.querySelector("#q").value.trim().toLowerCase();
    const rank = (a) => { const s = a.symbol.toLowerCase(); return s === q ? 0 : s.startsWith(q) ? 1 : 2; };
    const pop = (a) => { const i = POPULAR.indexOf(a.symbol); return i < 0 ? 99 : i; };
    const list = rows
      .filter((a) => net === "all" || a.network === net)
      .filter((a) => !q || a.symbol.toLowerCase().includes(q) || (a.name ?? "").toLowerCase().includes(q))
      .sort((a, b) => (q ? rank(a) - rank(b) : pop(a) - pop(b)) || a.symbol.localeCompare(b.symbol))
      .slice(0, 200);
    document.querySelector("#list").replaceChildren(...list.map((a) =>
      el("li", {}, el("button", { onclick: () => pick(a) },
        el("span", { className: "face" },
          img(a.icon, a.symbol, 32),
          el("span", { className: "badge" }, ...(a.networkIcon ? [img(a.networkIcon, a.networkName ?? "", 14)] : []))),
        el("b", { textContent: a.symbol }), " ", a.name ?? "", " ",
        el("small", { textContent: a.networkName ?? "" })))));
  }
  document.querySelector("#q").oninput = render;
  render();
</script>
```

A complete React version with keyboard support is in the [starter](https://www.myfloat.cc/docs/ai) (`components/TokenPicker.tsx`).

**The image maps.** The images are on www.myfloat.cc, and you may hotlink them or copy them into your own app. Both maps are public JSON, served with `Access-Control-Allow-Origin: *`:

- [https://www.myfloat.cc/data/token-icons.json](https://www.myfloat.cc/data/token-icons.json) — `{ "generatedAt": "…", "size": 64, "icons": { "USDT": "/icons/tokens/usdt.webp", … } }`. Keys are upper-case tickers; paths are relative to `https://www.myfloat.cc`. 64×64 WebP.
- [https://www.myfloat.cc/data/chain-logos.json](https://www.myfloat.cc/data/chain-logos.json) — `{ "generatedAt": "…", "size": 64, "chains": { "tron": { "name": "Tron", "logo": "/icons/chains/tron.png", "networks": ["TRON", "TRX", "TRC20", "TRC-20"] }, … }, "networks": { "TRON": "tron", "TRX": "tron", … } }`. `networks` maps every network code or alias to a chain id; `logo` is a 64×64 PNG or WebP path, or null.

`/v1/assets` already resolves both for you; read the maps only if you want to bundle the images.

## 4. Estimate — `POST /v1/quote`

A price that binds nothing. There is no quote id and no expiry: `POST /v1/order` re-quotes when it opens the order. Debounce it (about 500 ms) as the user types — each quote asks every partner.

```json
{ "direction": "in", "from": "USDT", "amount": "100", "rateType": "float" }
```

| Field | Required | |
|---|---|---|
| `direction` | no | `"in"` = asset → CC (default). `"out"` = CC → asset. |
| `from` | on `in` | The asset the user sends. |
| `to` | on `out` | The asset the user receives. |
| `amount` | yes | String, > 0. `in`: amount of `from`. `out`: amount of **CC**. |
| `rateType` | no | `"float"` or `"fixed"`. **Default `"fixed"`** — anything else is fixed. **Send `"float"`** (below). |
| `network` | no | Chain of the non-CC asset (see [Networks](#networks-and-pinned-chains)). May be omitted or `null` (or `""`) — all three mean the asset's default chain. |
| `destinationExtraId` | no, `out` only | The payout address's destination tag / memo. Send it on the quote too: partners that cannot carry it are left out of the ranking. See [Destination tag / memo](#destination-tag-memo-destinationextraid). |
| `timezone` / `tzOffset` | no | The end user's IANA zone / `Date.getTimezoneOffset()`; passed to partners whose risk checks ask for it. |

**Float or fixed?** A floating rate follows the market until the deposit confirms; a fixed rate is locked for the order. Fewer partners offer fixed rates and they price the risk in: in a live comparison on 17 Oct 2026, 100 USDT → CC fixed paid **about 1.7% less** than floating (936.73 vs 953.67 CC). **Recommend `"float"`** and offer fixed only if your users ask for it.

Response `200` (live, 17 Oct 2026):

```json
{
  "direction": "in", "from": "USDT", "to": "CC",
  "amountIn": "100",
  "amountOut": "953.672096269999997276",
  "venueOut": "953.672096269999997276",
  "markupBps": 60, "venueBaseFeeBps": 20, "totalFeeBps": 151,
  "venue": "thechange",
  "network": "TRON", "venueNetwork": "TRX", "networkChoice": "float_default",
  "settleSeconds": null, "warning": null,
  "needsRefundAddress": false,
  "alternatives": [
    { "venue": "exolix", "amountOut": "950.140842", "network": "TRON", "note": "0.37% less than The Change",
      "deltaPct": 0.37, "totalFeeBps": 187, "venueNetwork": "TRX", "needsRefundAddress": false }
  ],
  "venueSpread": 0.003702797097462964,
  "unavailable": [
    { "venue": "letsexchange", "reason": "venue returned no output amount", "minIn": "120" },
    { "venue": "rocketx", "reason": "rocketx settles USDT on ETHEREUM, not TRON" }
  ],
  "suggestedMin": null,
  "rateType": "float", "flatFee": "5", "flatFeeAsset": "out",
  "minIn": "30.050000000000000711", "maxIn": "2400032",
  "indicative": true
}
```

| Field | |
|---|---|
| `amountOut` | What the user receives, after every fee (partner, network, Float's). Show this. |
| `venue` | The winning partner's id. `alternatives` are the others, best first; let the user pick one and pass it as `venue` on the order. |
| `alternatives[].note` | A short comparison with the winner, naming it as people know it ("0.37% less than The Change"). Display text only — its wording may change; do not parse it. |
| `alternatives[].deltaPct` | The same comparison as a number: how much less this route delivers than the winner, in percent (`0.37`); `0` on a tie, `null` if it cannot be computed. Branch on this, not on `note`. |
| `network` / `venueNetwork` | The chain this price is on — Float's code / the partner's spelling. |
| `networkChoice` | `requested` (you sent `network` or a suffixed ticker), `float_default` (Float's pinned chain), `venue_default` (each partner's own). |
| `needsRefundAddress` | Get CC only: this partner (RocketX, or HoudiniSwap at a fixed rate) opens the order only once it has the user's refund address on the source chain. Collect it and send `refundAddress` with the order. Also on each alternative. Never affects ranking. |
| `destinationExtraIdNeeded` / `destinationExtraIdName` | Withdraw CC: whether payouts on this chain commonly need a tag / memo, and what this route calls it — **null when this route cannot pass one**. Also on each alternative. `false` / `null` on Get CC. Never affects ranking. |
| `totalFeeBps` | The all-in cost against market, already inside `amountOut`. `markupBps` is Float's share, `venueBaseFeeBps` the partner's published base fee — **`null` when the partner publishes none** (display it as "—", never as 0). |
| `flatFee` / `flatFeeAsset` | A flat fee the partner reports, in the input (`in`) or output (`out`) asset. Already inside `amountOut`; display only. |
| `minIn` / `maxIn` | The winning route's limits, in the input asset. Round `minIn` **up** and `maxIn` **down** for display. |
| `unavailable` | Partners that did not quote, with the reason (and `minIn` when size was the problem). |
| `suggestedMin` | When nothing quoted because the amount was too small, the smallest minimum reported. |
| `warning` | A sentence when the best price is notably worse than market. **Show it.** |
| `indicative` | Always `true`. |

**Partner ids** you may see in `venue`, `alternatives` and `unavailable`, with the name to show: `letsexchange` → **YNOT**, `thechange` → **The Change**, `exolix` → **Exolix**, `houdiniswap` → **HoudiniSwap**, `rocketx` → **RocketX**, `squid` → **Squid**, `stealthex` → **StealthEX**. Which are enabled changes over time; treat an unknown id as a name to show as-is.

**Too small or too large** is `409 conflict`, and the limit is **only in the message** — show `error` to the user (as text — see [Errors](#8-errors)):

```json
{ "error": "That amount is below every venue's minimum. The lowest is 30.0392 USDT.", "code": "conflict" }
{ "error": "That amount is above every venue's maximum. The most any will take is 29.38 BTC.", "code": "conflict" }
```

## 5. Create exchange — `POST /v1/order`

Re-quotes, opens the order at the partner, and returns where the user sends. Call it **from your server** with your key and an `Idempotency-Key`.

```http
POST /v1/order
Authorization: Bearer fl_live_YOUR_KEY_HERE
Idempotency-Key: 6f1c0d7e-4b8a-4f0e-9a51-2a8b3c4d5e6f
Content-Type: application/json

{
  "direction": "out",
  "to": "BTC",
  "amount": "1000",
  "rateType": "float",
  "destination": "bc1qexampleaddress…",
  "venue": "exolix",
  "refundParty": "alice::12207d5f1c3b9a0e4f6d8c2b1a0e9f8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b"
}
```

| Field | Get CC (`in`) | Withdraw CC (`out`) | |
|---|---|---|---|
| `direction` | `"in"` | `"out"` | Default `"in"`. |
| `from` | **required** | — | The asset the user sends. |
| `to` | — | **required** | The asset the user receives. |
| `amount` | **required** | **required** | String > 0. `in`: amount of `from`. `out`: amount of CC. |
| `destination` | **required** | **required** | Where the user is paid. `in`: their **Canton party ID** (`name::1220…`, 16+ hex after `::`). `out`: an address on the asset's chain (the partner validates it). Cannot be changed later. |
| `rateType` | optional | optional | Send `"float"`. |
| `network` | optional | optional | The chain of the non-CC asset — send the picker row's `network`. Omitted, `null` or `""`: the asset's default chain, as on `/quote`. |
| `venue` | optional | optional | Open at this partner (from the quote). If it is no longer quoting this pair at this size the order is refused (`409 conflict`) — never swapped for another. |
| `refundAddress` | optional (needed when `needsRefundAddress`) | — | The user's address on the **source** chain, for a failed swap's refund. Checked for that chain's format. |
| `refundParty` | — | optional, recommended | The user's **own Canton party**. Every automatic refund of the order's CC goes there. **Ask for it whenever the user may pay from an exchange.** Must be able to receive CC (a transfer preapproval) or the order is `422`. |
| `ref` | optional | optional | A referral code, used only when no valid key is sent. |
| `timezone` / `tzOffset` | optional | optional | As on `/quote`. |
| `destinationExtraId` | — (`400`) | optional | The payout address's **destination tag / memo**, when it needs one (an exchange's XRP, XLM, TON, ATOM… address). String; an integer is accepted for an XRP tag. Validated per chain. See [below](#destination-tag-memo-destinationextraid). |

Sending `refundAddress` on `out`, or `refundParty` on `in`, is `400 invalid_request`. Float's own deposit addresses are refused as `destination` or `refundParty`.

Response `201` — the order (Withdraw CC, waiting for the user's CC):

```json
{
  "claimToken": "3k0x9q…(64 characters)",
  "direction": "out", "from": "CC", "to": "BTC",
  "amountIn": "1000", "expectedOut": "0.00124738",
  "venue": "exolix",
  "depositAddress": "float-deposit-012::12207d5f1c3b9a0e4f6d8c2b1a0e9f8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
  "depositExtraId": null,
  "deliversTo": "bc1qexampleaddress…",
  "status": "pending", "state": "awaiting_deposit",
  "network": "BITCOIN", "venueNetwork": "BTC",
  "custodialState": "awaiting_deposit",
  "receivedCc": null, "receivedAt": null, "settledCc": null,
  "settledTxId": null, "venueTxId": null, "refundTxId": null,
  "createdAt": "2026-10-17T10:00:00.000Z",
  "deadline": 1792231200000,
  "depositDeadline": "2026-10-17T10:30:00.000Z",
  "depositGraceUntil": "2026-10-17T10:40:00.000Z",
  "transactions": [
    { "kind": "customer_deposit", "chain": "canton", "asset": "CC", "amount": "1000", "from": null,
      "to": "float-deposit-012::12207d5f…", "txId": null, "explorerUrl": null, "state": "pending", "at": null }
  ],
  "refundParty": "alice::12207d5f1c3b9a0e4f6d8c2b1a0e9f8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
  "destinationExtraId": null,
  "depositParty": "float-deposit-012::12207d5f1c3b9a0e4f6d8c2b1a0e9f8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
  "depositValidForThisOrderOnly": true,
  "depositAddressNote": "Valid for this order only"
}
```

A Get CC order looks the same with `from` the asset, `to` `"CC"`, `depositAddress` the partner's address on the source chain, `depositGraceUntil: null`, and `refundAddress`, `refundAddressRequired`, `refundAddressAtVenue` (and, when Float pays the user from a per-order wallet, `payoutParty` and `payoutFromThisOrderOnly` — informational; the user never sends there).

### How your user pays

**Withdraw CC (CC → X): the user pays Float.**

- `depositAddress` is **this order's own Float deposit address** — a Canton party used by this order alone. Show `depositAddressNote` ("Valid for this order only") next to it; the user must not reuse it.
- Send **exactly `amountIn` CC**. **No memo, ever** — `depositExtraId` is always null on CC → X. Float forwards the CC to the swap partner itself, with whatever reference that partner needs.
- Send before **`depositDeadline`**: Float's window is **30 minutes** from `createdAt` (earlier if the partner's own deadline is earlier). A transfer already in flight is still accepted until **`depositGraceUntil`**, 10 minutes later.
- **Short** (less than `amountIn`): the whole payment is refunded and the order cancelled — there are no top-ups.
- **Over** (more than `amountIn`): the order goes ahead for `amountIn` and the excess is refunded.
- **Late** (after the grace, or to an order that is already closed): refunded.
- Refunds go to the order's `refundParty` when it has one, otherwise back to the sending party. A refund to an exchange's shared deposit party is held for a person (a transfer there without the user's own memo can be lost) — which is why you should collect `refundParty`. A payment of less than 5 CC to a deposit address is treated as spam and is not refunded.
- While `state` is `preparing_deposit_address`, `depositAddress` is `""` and `depositPreparing` is true: show "Preparing your deposit address…" and keep polling (every 5 s); it usually takes a minute or two.

**Get CC (X → CC): the user pays the swap partner.**

- `depositAddress` is the partner's address on the source chain (`network`). Send **exactly `amountIn`** of `from` on that chain before `depositDeadline` (the partner's deadline, or 30 minutes when it names none). There is no grace: the partner, not Float, is receiving.
- When **`depositExtraId` is not null**, the user **must** include it as the memo / tag / destination tag on the source chain, or the partner cannot match the payment.
- The partner's rules apply to a wrong amount or a late payment. If the route needed a refund address, a failed swap's deposit goes back there.
- CC is delivered to `destination` (the user's Canton party), which must be able to receive CC — a party without a transfer preapproval is refused at creation (`422 unprocessable`).

On both: show the figures **from the response** (`amountIn`, `depositAddress`, `network`, `depositDeadline`), not what you sent; show `depositDeadline` as a countdown computed from that field (not from when your page loaded); and store **`claimToken`** server-side — it is a bearer capability: whoever holds it can read the order.

### Destination tag / memo — `destinationExtraId`

On Withdraw CC the payout address may need a **destination tag or memo**: an exchange's XRP, XLM, TON, ATOM, EOS, HBAR… deposit address is shared by all its customers, and the tag is how the exchange credits the right one. **A payout sent to an exchange without its tag can be lost.** A personal wallet needs none.

**The field.** `destinationExtraId` — optional, a string (a JSON integer is also accepted for an XRP tag) — on `POST /v1/quote` and `POST /v1/order`, `direction: "out"` only, next to `destination`. Blank is the same as absent. On `"in"` it is `400 invalid_request` (that payout is a Canton party, which takes no tag). It is part of the order body, so it is part of the `Idempotency-Key` identity: a retry with a different or missing tag is `409 idempotency_key_reused`.

**Validation** (`400 invalid_request`, with the reason), by the payout chain:

| Chain | Called | Format |
|---|---|---|
| XRP Ledger | Destination tag | digits only, 0 – 4294967295 (uint32) |
| Stellar (XLM, and tokens on it) | Memo | text up to 28 characters, or a numeric ID up to 18446744073709551615 |
| TON (and jettons such as USDT on TON) | Memo (comment) | text up to 64 characters |
| Cosmos Hub (ATOM) and other Cosmos chains | Memo | text up to 64 characters |
| EOS, Hedera (HBAR), BNB Beacon (BEP2) | Memo | text up to 64 characters |
| Bitcoin, Ethereum and EVM chains, Tron, Solana, Litecoin, Dogecoin… | — | **refused**: these addresses take no tag |

Every tag: 1–64 visible ASCII characters, no spaces.

**Which assets need one:** `GET /v1/assets` rows and quotes carry `destinationExtraIdNeeded` and `destinationExtraIdName` (above). When `destinationExtraIdNeeded` is true, show the field, labelled with `destinationExtraIdName`, and this warning: **"Sending to an exchange? Add your destination tag/memo, or the funds may be lost."** An order without a tag is still accepted (the user may be sending to a personal wallet).

**Partners.** With a tag in the request, a partner that cannot deliver it for that asset (RocketX and Squid cannot pass payout tags) is **left out of the quote** and listed in `unavailable` with the reason; the best route that can carry it wins. A tag is never silently dropped: no order opens on a partner that cannot carry it. Naming such a partner in `venue` is `409 venue_unavailable`. If no partner can carry it, the quote / order is `409 conflict` ("No partner can pay XRP out with a destination tag or memo right now…"), with `unavailable` listing why.

**On the order.** `destinationExtraId` is echoed on Withdraw CC orders (the create response and `GET /v1/order/{claimToken}`): the tag the partner was given, or `null`. Absent on Get CC orders. It cannot be added or changed after the order opens — open a new order instead.

### Pay from your own Canton wallet

If your app **is** a Canton wallet, or integrates one, Withdraw CC is one step for your user: send the CC straight from their wallet.

Every Float deposit address has a **TransferPreapproval**, so a standard Canton Coin transfer to `depositAddress` completes directly — there is no accept step on Float's side and no memo.

1. `POST /v1/order` with `direction: "out"`; keep `claimToken`.
2. Read `depositAddress`, `amountIn` and `depositDeadline` (if `state` is `preparing_deposit_address`, poll until the address appears).
3. Submit a CC transfer from the user's party to `depositAddress` for exactly `amountIn`, before `depositDeadline`. Set `refundParty` to the user's party when you create the order, so any refund comes straight back to the same wallet.
4. Wait for the `order.state_changed` webhook, or poll `GET /v1/order/{claimToken}`, until `state` is final.

```ts
// Wallet-agnostic sketch: `wallet.transferCC` is your wallet's own transfer call
// (a Canton Network Token Standard transfer / TransferPreapproval_Send).
const order = await float.createOrder({ direction: "out", to: "USDC", network: "ETHEREUM", amount: "500",
  rateType: "float", destination: userEvmAddress, refundParty: user.partyId });
const ready = await waitFor(() => float.getOrder(order.claimToken), (o) => o.depositAddress !== "");
await wallet.transferCC({ from: user.partyId, to: ready.depositAddress, amount: ready.amountIn });
await waitFor(() => float.getOrder(order.claimToken), (o) => ["completed", "refunded", "failed", "expired"].includes(o.state));
```

For the transfer itself, see the Splice documentation on the Canton Network Token Standard (CIP-0056) transfer API and TransferPreapproval: [https://docs.sync.global/](https://docs.sync.global/).

### When a partner will not open the order

A partner that quoted the route but will not open it answers `409 venue_unavailable`, with its id in `venue`. Nothing was opened or sent. Mark that route unavailable for this attempt and open the next one from `alternatives` (pass it as `venue`), or re-quote:

```json
{ "error": "The Change can't open this swap right now. Pick another route — nothing has been sent.", "code": "venue_unavailable", "venue": "thechange" }
```

A partner's own `4xx` (for example an address it will not pay) is `400 venue_refused` with its message — usually something the user can fix.

## 6. Exchange status — `GET /v1/order/{claimToken}`

Returns the same order object. Each read refreshes the order from its partner, so `state` moves forward between reads. An unknown token is `404 not_found`.

**Webhooks or polling:** with a [webhook](#6b-webhooks) registered, read the order when an `order.state_changed` event arrives (its `data.order` is this same object) and keep a slow poll as a fallback. Without one, poll every **10–30 seconds** while the user is watching (every 5 s during `preparing_deposit_address`), and stop at a final state. Reads have their own, generous limit (300 a minute per key).

**Drive your UI from `state`** — one field, ten values. `status` and `custodialState` are internal and kept only for compatibility; do not branch on them.

```text
[awaiting_refund_address | preparing_deposit_address →] awaiting_deposit → confirming → exchanging → sending → completed
                                                                                       ↘ failed | refunded | expired
```

| `state` | Meaning | What your app should do |
|---|---|---|
| `awaiting_refund_address` | Get CC: the route needs the user's source-chain refund address before the partner order opens. No deposit address yet. | Ask for the address; `POST /refund-address`. Do not ask the user to send anything. Unanswered, the order expires after 24 hours. |
| `preparing_deposit_address` | Withdraw CC: Float is creating this order's deposit address. `depositAddress` is `""`. | Show "Preparing your deposit address…"; poll every 5 s. |
| `awaiting_deposit` | Waiting for the user's payment. | Show the deposit step and the countdown. After `depositDeadline` (with nothing received) say "Deposit window closed — don't send now". |
| `confirming` | The payment was seen and is being confirmed / forwarded. | Show progress; nothing for the user to do. |
| `exchanging` | The partner is swapping. | Show progress. |
| `sending` | The payout to the user is on its way. | Show progress. |
| `completed` | The user has been paid. **Final.** | Show "Done" with `expectedOut` (or `settledCc`). |
| `refunded` | The payment went back to the user. **Final.** | Show where it went (`refundParty`, or the sending wallet). |
| `failed` | The swap will not complete. **Final** — except that when Float holds the user's deposit and is returning it, the order moves on to `refunded`. | Tell the user Float is on it; offer support@myfloat.cc with the order link. |
| `expired` | The deposit window closed with nothing received. **Final.** | "Nothing was received. Nothing was sent and nothing is owed — start a new order." |

`confirming`, `exchanging` and `sending` partly come from the partner's own status word, read live. Until a partner reports progress — or when it does not answer — an order shows `awaiting_deposit`, so treat the in-progress states as progress information and only the final states as facts.

**`transactions`** lists every on-chain transfer behind the order, oldest first:

| Field | |
|---|---|
| `kind` | `customer_deposit` (the user pays), `venue_payment` (Float pays the partner, CC → X), `venue_payout` (the partner pays out), `customer_payout` (Float pays the user, X → CC), `refund` |
| `chain`, `asset`, `amount` | Where and what. `chain` is `canton` or the partner's chain code. |
| `from`, `to`, `txId` | Parties / addresses and the transaction id, when known. |
| `explorerUrl` | A link to show ("View transaction"), when known — Canton transfers link to ccview.io. |
| `state` | `pending`, `confirmed`, `failed` or `cancelled`. |
| `at` | ISO time, when known. |

Other order fields: `receivedCc` / `receivedAt` (what Float received), `settledCc` / `settledTxId` (Float's payout to the user on Get CC), `venueTxId` (Float's payment to the partner on Withdraw CC), `refundTxId` — Canton update ids a user can look up, or null. `deadline` is `depositDeadline` in unix ms, kept for compatibility.

## 6b. Webhooks

Float can tell your server when something changes, so you do not have to poll
`GET /v1/order/{claimToken}`:

| Event | Sent when |
|---|---|
| `order.state_changed` | an order attributed to you (created with your API key, or through one of your referral links) changes `state` |
| `payout.updated` | one of your affiliate payouts is created or changes `status` |
| `webhook.test` | you press **Send test event** (or call `POST /v1/webhooks/{id}/test`) — always delivered to that endpoint, whatever its `events` |

Polling still works and stays the source of truth: a webhook tells you **that**
an order moved; `GET /v1/order/{claimToken}` always tells you where it is now.

### Register an endpoint

In the dashboard: **API for developers → Webhooks → Add endpoint**. Or with
your API key:

```bash
curl -X POST https://api.myfloat.cc/v1/webhooks \
  -H "Authorization: Bearer fl_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/float"}'
```

```json
{
  "endpoint": {
    "id": "whe_3f9c0a…",
    "url": "https://example.com/hooks/float",
    "status": "active",
    "pausedReason": null,
    "pausedAt": null,
    "secretHint": "whsec_…Q2aZ",
    "previousSecretValidUntil": null,
    "consecutiveFailures": 0,
    "failingSince": null,
    "lastSuccessAt": null,
    "lastFailureAt": null,
    "createdVia": "api_key:3f2b9c1e-7a4d-4e8b-9c0f-5d6e7a8b9c0d",
    "createdAt": "2026-10-17T12:00:00.000Z",
    "updatedAt": "2026-10-17T12:00:00.000Z",
    "events": ["order.state_changed", "payout.updated"]
  },
  "secret": "whsec_hY3…Q2aZ",
  "notice": "Store this signing secret now — it is shown only in this response. …"
}
```

| Endpoint field | |
|---|---|
| `status` | `active` or `paused`. `pausedReason` (`manual`, or why Float paused it) and `pausedAt` are set only while paused, otherwise `null`. |
| `consecutiveFailures`, `failingSince` | Failed real deliveries in a row, and when the current run of failures began (`null` when the last delivery succeeded). Drive auto-pause. |
| `lastSuccessAt`, `lastFailureAt` | The last successful / failed delivery of a **real** event. Test events change none of the health fields. |
| `createdVia` | `dashboard`, or `api_key:<key id>` — the id (a UUID) of the API key that registered the endpoint, as listed in the dashboard; never the key itself. |
| `createdAt`, `updatedAt` | ISO 8601 UTC. `updatedAt` is the last change to the endpoint itself. |
| `events` | The subscribable event types: `order.state_changed` and `payout.updated` — every endpoint receives all of them. `GET /v1/webhooks` also returns the same list as `eventTypes`. `webhook.test` is not in that list: it is sent only by `POST /v1/webhooks/{id}/test`, and always delivered there, whatever `events` says. |

**The `secret` is in this response and no other.** Store it with your other
credentials. If you lose it, rotate it ([Rotating the secret](#rotating-the-secret)).

| Call | What it does |
|---|---|
| `GET /v1/webhooks` | your endpoints (never the secrets) |
| `POST /v1/webhooks` `{ url }` | register; returns the secret once |
| `GET /v1/webhooks/{id}` | one endpoint |
| `PATCH /v1/webhooks/{id}` `{ url?, paused? }` | change the URL; `paused: true` pauses, `paused: false` resumes |
| `DELETE /v1/webhooks/{id}` | remove it (its undelivered events are dropped) |
| `POST /v1/webhooks/{id}/rotate-secret` | new secret, returned once; the old one keeps working for 24 h |
| `POST /v1/webhooks/{id}/test` | send a `webhook.test` now and get the receiver's status code back |
| `GET /v1/webhooks/{id}/deliveries` | the last 50 delivery attempts, with status codes |

The dashboard uses the same calls under `/api/float/partner/webhooks`,
authenticated by your signed-in session.

**Rules for the URL.** `https://` only; no user name or password in it; no IP
addresses in private, loopback, link-local or other reserved ranges; no
`localhost`, `.local`, `.internal` or single-label names; no cloud metadata
addresses; not a Float host. Up to **5 endpoints** per account, each URL once.
Every endpoint receives every event type; ignore types you do not handle (new
types may be added).

**API key or dashboard?** Both can manage endpoints, so an integration (or an
AI doing the integration from these docs) can set itself up with only the key.
Because an endpoint receives your orders' claim tokens, every change made
**with a key** is emailed to your account address straight away, naming the
key and the URL. If you did not make it, revoke the key and delete the
endpoint. Restricting your key to your servers' IPs (dashboard → the key →
API access restrictions) also applies to these calls.

### What you receive

Every delivery is a `POST` with a JSON body and these headers:

| Header | Value |
|---|---|
| `Content-Type` | `application/json; charset=utf-8` |
| `Float-Signature` | `t=<unix seconds>,v1=<hex>` — see [Verify the signature](#verify-the-signature) |
| `Float-Event-Id` | the event's `id` — the same on every retry |
| `Float-Event-Type` | e.g. `order.state_changed` |
| `Float-Delivery-Attempt` | `1` on the first try, `2` on the first retry, … |
| `User-Agent` | `FloatWebhooks/1.0 (+https://myfloat.cc/docs)` |

Every body has the same envelope:

| Field | Meaning |
|---|---|
| `id` | `evt_…` — unique per event, **identical on every retry**. De-duplicate on this. |
| `type` | the event type |
| `created_at` | when the change was seen (ISO 8601 UTC) — not when this POST was made |
| `data` | the event's content, below |

#### `order.state_changed`

```json
{
  "id": "evt_7c1e5b0a9f2d4c3b8a1e6f0d",
  "type": "order.state_changed",
  "created_at": "2026-10-17T12:04:31.512Z",
  "data": {
    "previous_state": "awaiting_deposit",
    "order": {
      "claimToken": "…",
      "direction": "in",
      "from": "BTC",
      "to": "CC",
      "amountIn": "0.01",
      "expectedOut": "7624.1",
      "venue": "letsexchange",
      "depositAddress": "bc1q…",
      "depositExtraId": null,
      "deliversTo": "yourparty::1220…",
      "status": "pending",
      "state": "confirming",
      "network": "BITCOIN",
      "venueNetwork": "BTC",
      "…": "every other field of GET /v1/order/{claimToken}"
    }
  }
}
```

`data.order` is the object `GET /v1/order/{claimToken}` returns — same
serializer, same fields, one schema — built from what Float has stored (a
`GET` can be a step ahead where it asks the exchange partner live). Read `data.order.state`;
`previous_state` is what Float last told you (`null` on the first event for an
order). The states:

`awaiting_refund_address`, `preparing_deposit_address`, `awaiting_deposit`,
`confirming`, `exchanging`, `sending`, then one of the final states
`completed`, `refunded`, `failed`, `expired`.

- **You may not see every state.** An order that moves through several between
  two of Float's looks (they are about a minute apart) produces one event,
  e.g. `awaiting_deposit → completed`. On orders where the exchange partner
  holds every leg, `confirming` / `exchanging` / `sending` come from that
  partner's live status and are usually skipped in events (a `GET` may still
  show them). Act on `state`, never on a sequence.
- **`state` never goes backwards** in events.
- **Final states are final**, with one documented exception: a custodial order
  can go `failed → refunded` once Float returns the deposit.
- The first event for an order can arrive up to a minute or two after you
  created it. Orders you created before registering any endpoint that were
  already finished by then produce no event.

#### `payout.updated`

```json
{
  "id": "evt_0b9d…",
  "type": "payout.updated",
  "created_at": "2026-10-17T12:10:02.004Z",
  "data": {
    "previous_status": "requested",
    "payout": {
      "id": "…",
      "amountCc": "125.400000",
      "destination": "yourparty::1220…",
      "status": "sent",
      "failureReason": null,
      "createdAt": "2026-10-17T12:09:40.118Z",
      "updatedAt": "2026-10-17T12:10:01.870Z"
    }
  }
}
```

`payout` carries the fields the dashboard's payout list shows, plus
`updatedAt`. `previous_status` is `null` for a payout's first event.

#### `webhook.test`

```json
{
  "id": "evt_…",
  "type": "webhook.test",
  "created_at": "2026-10-17T12:00:00.000Z",
  "data": { "endpoint_id": "whe_…", "message": "This is a test event from Float. …" }
}
```

Signed exactly like a real event. Use it to check your verification code.

### Verify the signature

`Float-Signature` is `t=<unix seconds>,v1=<hex>`. The hex is
**HMAC-SHA256** keyed with your endpoint's secret (the whole `whsec_…` string,
as text) over:

```
<t> + "." + <the raw request body>
```

To verify: take the raw body **exactly as received** (not re-serialised JSON),
compute the HMAC for the header's `t`, compare it in constant time with each
`v1` value (there are two for 24 h after a rotation — accept if either
matches), and reject a `t` more than 5 minutes from your clock (replays).

#### Node

```js
import crypto from "node:crypto";

// rawBody: the request body as a Buffer or string, byte-for-byte as received.
export function verifyFloatWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  let t = null;
  const sigs = [];
  for (const part of header.split(",")) {
    const i = part.indexOf("=");
    if (i < 0) continue;
    const k = part.slice(0, i).trim();
    const v = part.slice(i + 1).trim();
    if (k === "t" && /^\d+$/.test(v)) t = Number(v);
    else if (k === "v1") sigs.push(v);
  }
  if (t === null || sigs.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  return sigs.some((s) => {
    const got = Buffer.from(s, "hex");
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}
```

With Express, keep the raw body for this route:

```js
app.post("/hooks/float", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyFloatWebhook(req.body, req.get("Float-Signature"), process.env.FLOAT_WEBHOOK_SECRET)) {
    return res.status(400).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // de-duplicate on event.id, queue the work, answer fast:
  res.status(200).end();
});
```

#### Python

```python
import hashlib
import hmac
import time


def verify_float_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    """raw_body: the request body bytes exactly as received."""
    if not header:
        return False
    t, sigs = None, []
    for part in header.split(","):
        k, sep, v = part.strip().partition("=")
        if not sep:
            continue
        if k == "t" and v.isdigit():
            t = int(v)
        elif k == "v1":
            sigs.append(v)
    if t is None or not sigs or abs(int(time.time()) - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)
```

(Flask: `request.get_data()`; Django: `request.body`; FastAPI:
`await request.body()`.)

### Answering, retries and timeouts

- **Success is any `2xx`** within **10 seconds**. Answer first, do the work
  afterwards (a queue), or slow work will turn into retries.
- **Redirects are not followed** — a `3xx` counts as a failure. Register the
  final URL.
- Float never reads your response body.
- **Anything else is retried** with exponential backoff — about 30 s, 1 min,
  2, 4, 8, 16, 32 min, 1 h, 2 h, then every 4 h (±10 % jitter) — for **24 hours
  from the event**, with a last try at the 24-hour mark. Then that event is
  given up for that endpoint (it shows as failed in the delivery log; the
  order itself is unaffected — `GET /v1/order/{claimToken}` still has it).
- **Auto-pause.** An endpoint that fails **10 times in a row over at least 12
  hours**, with no success in between, is paused and your account email is
  told. While paused it receives nothing new; events that were already queued
  for it keep their 24 hours and go out as soon as you resume it. Fix the
  receiver, send a test event, then resume (`PATCH … {"paused": false}` or the
  dashboard). You can pause an endpoint yourself the same way.

### Ordering and idempotency

- **Idempotency:** the event `id` (also in `Float-Event-Id`) is the same on
  every retry. Store the ids you have processed and ignore repeats — a
  delivery can arrive twice if your `2xx` was lost on the way back.
- **Ordering:** for **one order**, events reach an endpoint in the order they
  happened: a later event waits while an earlier one for the same order is
  still being retried. Across different orders there is no ordering. If you
  ever need certainty, compare `data.order.state` with what you stored, or
  fetch the order.

### Test events

Dashboard: **Send test event** on the endpoint. API:
`POST /v1/webhooks/{id}/test` — it sends one `webhook.test` immediately and
answers with what your server said:

```json
{ "eventId": "evt_…", "delivered": true, "statusCode": 200, "error": null, "durationMs": 182 }
```

Test events work on paused endpoints, are delivered whatever the endpoint's
`events`, are never retried, never count toward auto-pause, and appear in the
delivery log. They do not change the endpoint's health: `lastSuccessAt`, `lastFailureAt`, `consecutiveFailures` and
`failingSince` move only on real events. Limited to 6 a minute.

### Rotating the secret

`POST /v1/webhooks/{id}/rotate-secret` (or **Rotate secret**) returns a new
secret, once. For the next **24 hours** every delivery is signed with both:
`Float-Signature: t=…,v1=<new>,v1=<old>` — so a receiver still holding the old
secret keeps verifying while you deploy the new one. After 24 h only the new
secret signs. `previousSecretValidUntil` on the endpoint says when the overlap
ends.

### Delivery log

`GET /v1/webhooks/{id}/deliveries` (and the dashboard) lists the last 50
attempts, newest first:

```json
{ "attempts": [
  { "eventId": "evt_…", "eventType": "order.state_changed", "attempt": 2,
    "delivered": false, "statusCode": 503, "error": "HTTP 503", "durationMs": 412,
    "at": "2026-10-17T12:05:02.000Z" }
] }
```

`error` is Float's own description (`HTTP 503`, `timeout — no answer within
10 s`, `network — …`, `HTTP 302 — redirects are not followed`), never your
server's response.

## 7. Refunds — `POST /v1/order/{claimToken}/refund-address`

The claim token is the only credential. Body: `{ "address": "…", "acceptOut": "…" }` (`acceptOut` optional).

**Get CC — the user's refund address on the source chain.**

| Order is… | Result |
|---|---|
| waiting for a refund address | Stored, and the partner order **opens** on a fresh quote from the same partner and chain: `200 { "opened": true, "venueHasAddress": true, "message": "…", "order": { … } }` — the order now has its deposit address. |
| … and the fresh quote pays more than 0.5% less than the user saw | `409 requote_needed` with `requote: { venue, previousOut, freshOut }`. Nothing opened. Show `freshOut`; to accept, re-send with `"acceptOut": "<freshOut>"`. |
| already open | Stored for support (`opened: false`); the partner keeps what it was given at creation. |
| expired or finished | `409 conflict`. |

**Withdraw CC — the user's Canton refund party.** `{ "address": "name::1220…" }` sets or changes `refundParty`. It moves nothing and opens nothing: `200 { "opened": false, "venueHasAddress": false, "message": "…", "order": { …, "refundParty": "…" } }`. Allowed only until a refund of the order has started; afterwards (or on a finished order) it is `409 conflict`. A party without a transfer preapproval is `422`.

This call counts against the per-key **order** limit, because it can open a partner order.

**The deposit-window rules, in one place**

| What happened | Withdraw CC (user pays Float) | Get CC (user pays the partner) |
|---|---|---|
| Exact amount, on time | Processed | Processed |
| Less than `amountIn` | Refunded in full, order cancelled | The partner's rules |
| More than `amountIn` | `amountIn` processed, excess refunded | The partner's rules |
| After `depositDeadline`, before `depositGraceUntil` | Still processed | — (no grace) |
| After the grace, or after the order closed | Refunded | The partner's rules |
| Nothing received | `expired` after the grace | `expired` when the partner reports it |
| Refunds go to | `refundParty`, else the sending party (held for a person if that is an exchange's shared party) | `refundAddress` when the route has one |

## 8. Errors

Every error is JSON:

```json
{ "error": "Enter an amount.", "code": "invalid_request" }
```

`error` is a sentence for a person and may be reworded — show it, but **branch on `code`**.

**Display errors as text, never innerHTML.** Put `error` on the page with `textContent` (or your framework's ordinary text binding), never `innerHTML` or `dangerouslySetInnerHTML`. Float's own sentences never repeat what you sent — an asset Float does not list is answered `409 conflict` with exactly `Unknown asset. Use a symbol from GET /v1/assets.` — but treat every `error` as untrusted text all the same.

| HTTP | `code` | Meaning | What your app should do |
|---|---|---|---|
| 400 | `invalid_request` | Missing or bad field: no JSON body, no asset, amount not > 0, bad `network`, a suffixed ticker that disagrees with `network`, bad `destination` / `refundAddress` / `refundParty`, CC as the non-CC asset | Show `error`; fix the input |
| 400 | `venue_refused` | The partner rejected the order (its message is included) | Show it; usually the address or amount — fix and retry, or pick another route |
| 400 | `idempotency_key_invalid` | `Idempotency-Key` is not 1–255 visible ASCII characters | Use a UUID |
| 400 | `idempotency_requires_key` | `Idempotency-Key` sent without an API key | Send your key (or drop the header) |
| 401 | `invalid_key` | The key is unknown, revoked, or used from an IP it does not allow | Check the key and its IP list in the dashboard |
| 404 | `not_found` | Unknown claim token, referral code or path | — |
| 409 | `conflict` | No route at this size (limit in `error`), an asset Float does not list ("Unknown asset. Use a symbol from GET /v1/assets."), the chosen `venue` is not quoting it, or the order is expired / finished / already refunding | Show `error`; re-quote, adjust the amount, or pick a symbol from `/v1/assets` |
| 409 | `venue_unavailable` | The partner that quoted this route would not open the order. Body has `venue`. Nothing was opened | Mark that route unavailable; open the next from `alternatives`, or re-quote |
| 409 | `requote_needed` | Adding a refund address would open the order more than 0.5% worse. Body has `requote` | Show `requote.freshOut`; re-send with `acceptOut` to accept |
| 409 | `idempotency_key_reused` | Same `Idempotency-Key`, different body | Use a new key for a new order |
| 409 | `idempotency_in_progress` | The first request with this key is still opening the order | Retry the same request after `Retry-After` |
| 422 | `unprocessable` | The Canton party (`destination` on Get CC, `refundParty` on Withdraw CC) has no transfer preapproval | Ask the user to enable receiving in their wallet, then retry |
| 429 | `rate_limited` | Too many requests | Wait `Retry-After` seconds; retry the identical request |
| 502 | `upstream_error` | A partner did not answer, or the order could not be recorded | Re-quote and retry; on `POST /order`, retry with the same `Idempotency-Key` |
| 503 | `unavailable` | Float is switched off, **deposits are paused** ("Deposits are paused right now…"), every deposit address is in use, a deposit address could not be prepared, or the key could not be checked. Nothing was opened | Show `error`; retry later |

The `/v1/webhooks` calls also use `unauthorized` (401, no `fl_` key sent — these calls need one), `forbidden` (403), `invalid_url` (400), `too_many_endpoints` and `duplicate_url` (409) and `internal` (500). Your client should treat any unknown `code` by its HTTP status class.

## 9. Earnings

- **0.2% (20 bps) of the swap's CC amount** — the CC the partner delivers on Get CC, the CC the user sends on Withdraw CC. It comes out of Float's own fee: your user gets exactly the price they would get on myfloat.cc.
- **Attribution** is decided when the order opens and never changes: your API key if the request carries a valid one, otherwise a referral code (`ref`). `GET /v1/r/{code}` resolves a referral code to `{ "ref": "<code>", "partner": "<your app name>" }` and counts one click on it (`404 not_found` for an unknown code); the embed widget calls it for you.
- Each attributed order writes one earning: `accrued` when the order opens → `payable` once the order **completes** (Float's background jobs promote it, usually within minutes) → `paid` when included in a payout; `void` if the order fails, is refunded or expires. Only `payable` is withdrawable.
- **Payouts** are requested in the dashboard ([https://www.myfloat.cc/partners](https://www.myfloat.cc/partners) → **Withdrawal**) and sent in CC, automatically, from **Float's affiliate wallet** to the payout Canton party you set in **Settings** (it must be able to receive CC — a transfer preapproval). The minimum payout is **10 CC** by default; below it **Withdraw** is disabled. Ask for any amount from the minimum up to your payable balance and exactly that is sent (earnings are taken **oldest first**, and the last order's earning is split if needed; leave the amount empty to withdraw everything payable), and a new payout waits behind any earlier payout that is still held (`pending_funds` or `unknown`).
- **Dashboard figures** (the partner dashboard's earnings summary): `earnedCc` = `paidCc` + `pendingCc` + `availableCc` + `settlingCc`, where `paidCc` is CC actually **sent** to you, `pendingCc` is in payouts that are requested, `pending_funds` or `unknown`, `availableCc` is `payable` (withdrawable now), and `settlingCc` is earned on completed orders and not yet payable.
- Payout states: `sent`; `pending_funds` (the affiliate wallet is being topped up — the amount is held and paid automatically, retried every 5 minutes); `failed` (nothing moved; the earnings are payable again — for example your party cannot receive CC yet); `unknown` (Float could not confirm the transfer; the amount stays held, never paid twice, until Float checks it — contact support@myfloat.cc).

## 10. Embed widget

The no-code alternative: Float's own swap screen in an iframe on your page, attributed with your **referral code** — never an API key. Create a code in the dashboard (**Referral links**).

```html
<iframe src="https://www.myfloat.cc/bridge?embed=1&ref=YOUR_CODE" style="width:100%;border:0" title="Swap to CC"></iframe>
<script>
  addEventListener("message", (e) => {
    if (e.origin === "https://www.myfloat.cc" && e.data?.source === "float-widget" && e.data.type === "height")
      document.querySelector("iframe[title='Swap to CC']").style.height = e.data.height + "px";
  });
</script>
```

| Parameter | |
|---|---|
| `embed=1` | Drops Float's header and footer, and posts [messages](#messages-from-the-widget) (its height, the direction, the order's state — never its claim token) to the parent page. |
| `ref` | Your referral code (letters, digits, `_`, `-`; up to 64). Orders are attributed to you; each visit counts one click (once per browser tab). |
| `dir` | `in` (Get CC, default) or `out` (Withdraw CC). |
| `asset` | The non-CC asset to start with, e.g. `USDT`. |
| `amount` | The amount to start with, e.g. `250`. Without it the widget starts at **250 USDT** on Get CC and **500 CC** on Withdraw CC. An `amount` or `asset` you pass wins, and is kept when the user switches between Get CC and Withdraw CC. |
| `order` | A `claimToken`: reopens that order's status screen (for "view your order" links). |

Use `www.myfloat.cc` exactly — the bare domain redirects there. A plain referral link is the same page without `embed=1`: `https://www.myfloat.cc/bridge?ref=YOUR_CODE`.

### Messages from the widget

With `embed=1` the widget posts these to the parent page with `postMessage` (target origin `"*"`, so **your listener must check `e.origin === "https://www.myfloat.cc"` and `e.data.source === "float-widget"`** and ignore anything else):

| `type` | Message | Sent |
|---|---|---|
| `height` | `{ source: "float-widget", type: "height", height: <pixels> }` | whenever the content's height changes |
| `direction` | `{ source: "float-widget", type: "direction", dir: "in" \| "out" }` | on load, and whenever the user switches between **Get CC** (`in`) and **Withdraw CC** (`out`) |
| `order_created` | `{ source: "float-widget", type: "order_created", direction: "in" \| "out" }` | when an order opens |
| `order_state` | `{ source: "float-widget", type: "order_state", state }` | when an order is shown (on load of `?order=…` or right after it opens) and on every change of its `state` |

- `state` is the order's public `state`, the same lifecycle as the API: `awaiting_refund_address`, `preparing_deposit_address`, `awaiting_deposit`, `confirming`, `exchanging`, `sending`, then `completed`, `refunded`, `failed` or `expired` ([Exchange status](#6-exchange-status-get-v1orderclaimtoken)).
- **The widget never posts the order's claim token** (nor any other identifier that can act on the order). Anything on your page can read these messages, so they carry nothing that could view or change an order. The order link stays with the user, inside the widget.
- No secrets and no personal data are posted — no addresses, no amounts, no API key.
- New message types may be added; ignore types you do not handle.

```html
<script>
  const FLOAT_ORIGIN = "https://www.myfloat.cc";
  const frame = document.querySelector("iframe[title='Swap to CC']");
  addEventListener("message", (e) => {
    if (e.origin !== FLOAT_ORIGIN || e.data?.source !== "float-widget") return;
    const m = e.data;
    switch (m.type) {
      case "height":        frame.style.height = m.height + "px"; break;
      case "direction":     document.body.dataset.floatDir = m.dir; break;                 // "in" | "out"
      case "order_created": trackOrderOpened(m.direction); break;                         // your own code, e.g. analytics
      case "order_state":   if (m.state === "completed") showThanks(); break;             // your own code
    }
  });
</script>
```

## 11. Code examples

The whole flow — assets → quote → create order (with `Idempotency-Key`) → show the deposit → poll → done. **Your API key goes only in the server-side `FLOAT_API_KEY` environment variable.** The examples below never contain a real key; `fl_live_YOUR_KEY_HERE` is a placeholder.

### curl

```bash
BASE=https://api.myfloat.cc/v1
FLOAT_API_KEY=fl_live_YOUR_KEY_HERE   # server-side secret — never in a browser or a repo

# 1. Currencies (for a picker)
curl -s "$BASE/assets?all=1" | head -c 400

# 2. Estimate
curl -s -X POST "$BASE/quote" -H "Authorization: Bearer $FLOAT_API_KEY" -H "content-type: application/json" \
  -d '{"direction":"out","to":"BTC","amount":"1000","rateType":"float"}'

# 3. Create the order (one Idempotency-Key per intended order; reuse it on retries)
IDEM=$(uuidgen)
curl -s -X POST "$BASE/order" -H "Authorization: Bearer $FLOAT_API_KEY" -H "content-type: application/json" \
  -H "Idempotency-Key: $IDEM" \
  -d '{"direction":"out","to":"BTC","amount":"1000","rateType":"float","destination":"bc1q…","refundParty":"user::1220…"}'
# → show depositAddress, amountIn, depositDeadline; keep claimToken

# 4. Status, every 10–30 s until state is completed | refunded | failed | expired
curl -s "$BASE/order/$CLAIM_TOKEN" -H "Authorization: Bearer $FLOAT_API_KEY"
```

### JavaScript (Node 18+, fetch)

```js
// float.mjs — server-side only. Run: FLOAT_API_KEY=fl_live_YOUR_KEY_HERE node float.mjs
import { randomUUID } from "node:crypto";

const BASE = "https://api.myfloat.cc/v1";
const KEY = process.env.FLOAT_API_KEY; // <-- your key, from the environment only

async function float(path, { method = "GET", body, idempotencyKey } = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(BASE + path, {
      method,
      headers: {
        accept: "application/json",
        ...(KEY ? { authorization: `Bearer ${KEY}` } : {}),
        ...(body ? { "content-type": "application/json" } : {}),
        ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    const json = await res.json();
    if (res.ok) return json;
    // Retry what is safe to retry: rate limits, "still opening", and 5xx on an idempotent call.
    const retryable = res.status === 429 || json.code === "idempotency_in_progress" || (res.status >= 500 && (method === "GET" || idempotencyKey));
    if (retryable && attempt < 3) {
      await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 2) * 1000));
      continue;
    }
    throw Object.assign(new Error(json.error), { code: json.code, status: res.status, body: json });
  }
}

// 1. Currencies
const { assets } = await float("/assets?all=1");
const btc = assets.find((a) => a.symbol === "BTC" && a.network === "BITCOIN"); // symbol + network identify a row

// 2. Estimate (float rate)
const quote = await float("/quote", { method: "POST", body: { direction: "out", to: btc.symbol, network: btc.network, amount: "1000", rateType: "float" } });
console.log(`1000 CC → ${quote.amountOut} BTC via ${quote.venue}`);

// 3. Create the order — one Idempotency-Key per intended order, reused on every retry.
const idempotencyKey = randomUUID();
let order;
try {
  order = await float("/order", {
    method: "POST",
    idempotencyKey,
    body: { direction: "out", to: "BTC", network: btc.network, amount: "1000", rateType: "float",
            venue: quote.venue, destination: "bc1q…user-address", refundParty: "user::1220…" },
  });
} catch (e) {
  if (e.code === "venue_unavailable") { /* drop quote.venue, retry with quote.alternatives[0].venue and a NEW key */ }
  throw e;
}

// 4. Show the deposit
console.log(`Send exactly ${order.amountIn} ${order.from} to ${order.depositAddress} before ${order.depositDeadline}` +
  (order.depositExtraId ? ` with memo ${order.depositExtraId}` : "")); // CC → X: depositExtraId is always null

// 5. Poll until final
const FINAL = ["completed", "refunded", "failed", "expired"];
while (!FINAL.includes(order.state)) {
  await new Promise((r) => setTimeout(r, order.state === "preparing_deposit_address" ? 5_000 : 15_000));
  order = await float(`/order/${order.claimToken}`);
  console.log(order.state);
}
```

### Python (3.9+, requests)

```python
# float_flow.py — server-side only. pip install requests
# Run: FLOAT_API_KEY=fl_live_YOUR_KEY_HERE python float_flow.py
import os, time, uuid
from decimal import Decimal
import requests

BASE = "https://api.myfloat.cc/v1"
KEY = os.environ.get("FLOAT_API_KEY")  # <-- your key, from the environment only

def call(path, method="GET", body=None, idempotency_key=None):
    headers = {"accept": "application/json"}
    if KEY:
        headers["authorization"] = f"Bearer {KEY}"
    if idempotency_key:
        headers["idempotency-key"] = idempotency_key
    for attempt in range(4):
        r = requests.request(method, BASE + path, json=body, headers=headers, timeout=30)
        data = r.json()
        if r.ok:
            return data
        retryable = r.status_code == 429 or data.get("code") == "idempotency_in_progress" or (
            r.status_code >= 500 and (method == "GET" or idempotency_key))
        if retryable and attempt < 3:
            time.sleep(int(r.headers.get("retry-after", "2")))
            continue
        raise RuntimeError(f'{r.status_code} {data.get("code")}: {data.get("error")}')

# 1. Currencies
assets = call("/assets?all=1")["assets"]
usdt = next(a for a in assets if a["symbol"] == "USDT" and a.get("network") == "TRON")  # symbol + network identify a row

# network is optional: include the key only when the row has one (omitted = the asset's default network)
net = {"network": usdt["network"]} if usdt.get("network") else {}

# 2. Estimate — amounts are decimal strings: parse with Decimal, never float
q = call("/quote", "POST", {"direction": "in", "from": "USDT", **net, "amount": "100", "rateType": "float"})
print("100 USDT ->", Decimal(q["amountOut"]).quantize(Decimal("0.0001")), "CC via", q["venue"])

# 3. Create the order (Get CC: destination is the user's Canton party)
order = call("/order", "POST", {
    "direction": "in", "from": "USDT", **net, "amount": "100", "rateType": "float",
    "venue": q["venue"], "destination": "user::1220…",
    **({"refundAddress": "T…user-tron-address"} if q["needsRefundAddress"] else {}),
}, idempotency_key=str(uuid.uuid4()))

# 4. Show the deposit
memo = f' with memo/tag {order["depositExtraId"]}' if order["depositExtraId"] else ""
print(f'Send exactly {order["amountIn"]} {order["from"]} on {order["network"]} to {order["depositAddress"]}{memo} before {order["depositDeadline"]}')

# 5. Poll until final
while order["state"] not in ("completed", "refunded", "failed", "expired"):
    time.sleep(5 if order["state"] == "preparing_deposit_address" else 15)
    order = call(f'/order/{order["claimToken"]}')
    print(order["state"])
```

### Amounts are decimal strings

Every amount is a **decimal string**. Partner figures can carry binary-float artifacts (`"953.672096269999997276"`), so:

- parse with a decimal type (`Decimal` in Python, a decimal library or `BigInt` scaling in JavaScript) — never do money arithmetic in floating point;
- show what the user must **send** (`amountIn`) **exactly as returned** — never rounded;
- round what they **receive** (`amountOut`, `expectedOut`) **down** for display: about 4 decimals for CC, 2 for stablecoins, 8 for BTC, 6 otherwise;
- round a minimum (`minIn`) **up** and a maximum (`maxIn`) **down**.

## 12. Versioning and changelog

`/v1` is **stable**. Within `/v1` Float only adds: new endpoints, new optional request fields, new response fields, new error codes. Your client must ignore fields it does not know. Anything breaking ships under a new version (`/v2`) with notice. The original unversioned paths under `/api/float` still work but are not documented; use `/v1`.

Newest first.

| Date | Change |
|---|---|
| 2026-10-21 | Embed widget: `order_created` no longer carries `claimToken` — the widget never posts the order's claim token (or anything else that can act on an order) to the host page. |
| 2026-10-20 | Error sentences never repeat your input; an asset Float does not list is `409 conflict` "Unknown asset. Use a symbol from GET /v1/assets." `alternatives[].deltaPct`, and `alternatives[].note` names the winner as people know it ("The Change"). `/v1/assets` lists each pinned asset once (`USDT-TRC20`, `USDC-ERC20`… still accepted). `/v1` answers carry no CORS credentials (`/v1/openapi.json` stays readable from any origin). The OpenAPI `servers` list is `https://api.myfloat.cc/v1` only. Embed widget: posts `direction`, `order_created` and `order_state` messages to the host page besides `height`; default amounts per direction. Partner payouts send exactly the amount asked, from the minimum up to the payable balance. |
| 2026-10-17 | Public reference, OpenAPI 3.1 at `/v1/openapi.json` and www.myfloat.cc/openapi.json. `/v1/assets` rows carry Float-hosted `icon`, `networkIcon` and `networkName`. |
| 2026-10-17 | Withdraw CC orders always get their own deposit address (`depositValidForThisOrderOnly`), no memo; 30-minute deposit window plus 10-minute grace (`depositDeadline`, `depositGraceUntil`). |
| 2026-10-17 | Pinned chains (`networkChoice: "float_default"`), `409 venue_unavailable`. |
| 2026-10-17 | Webhooks: `order.state_changed`, `payout.updated`, `webhook.test`; `/v1/webhooks` endpoints (API key). |
| 2026-10-17 | Destination tag / memo: `destinationExtraId` on Withdraw CC quotes and orders; `destinationExtraIdNeeded` / `destinationExtraIdName` on `/assets` rows and quotes. |
| 2026-10-17 | Base URL is `https://api.myfloat.cc/v1`. |
| 2026-10-16 | `/v1` launched: per-key rate limits, `Idempotency-Key`, one `state` field, `refundAddress` / `refundParty`. |
