{
  "openapi": "3.1.0",
  "info": {
    "title": "CryptoExchanger API",
    "version": "1.0.0",
    "summary": "Non-custodial, no-KYC instant crypto swaps over a simple REST API.",
    "description": "Automate crypto swaps from your own program. You ask for a quote, create an order, and we return a **deposit address**; your program sends the exact amount to it and we execute the swap and pay out to the recipient you named.\n\n**We are non-custodial.** We never connect to, hold, or sign for your wallet or your keys. Funds move from your wallet, to the deposit address, to the recipient — we only route the exchange.\n\n### Flow\n1. `GET /estimate` — get a quote (rate, min, max, amount out).\n2. `POST /exchange` — create an order. We return a `payinAddress` (+ memo/tag if the coin needs one) and the exact `expectedFromAmount` to send.\n3. Send the deposit from your wallet.\n4. `GET /order/{id}` — poll until `status` is `finished` (or subscribe to a webhook).\n\n### Why us\n- **Idempotency-Key** on order creation — a retried request never mints a second deposit address (most swap APIs don't offer this).\n- **HMAC-signed webhooks** — get pushed status updates you can cryptographically verify, instead of polling.\n- **0% markup** by default — you get the raw provider rate; set your own partner markup if you want to earn.",
    "contact": {
      "name": "CryptoExchanger",
      "url": "https://cryptoexchanger.co"
    }
  },
  "servers": [
    {
      "url": "https://cryptoexchanger.co/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Reference",
      "description": "Coins, networks and quotes. Reads work without a key, at a low per-IP rate limit."
    },
    {
      "name": "Exchange",
      "description": "Create and track swap orders."
    },
    {
      "name": "Portal",
      "description": "Self-serve key management (base host, not /api/v1): mint a key, set a webhook."
    }
  ],
  "paths": {
    "/currencies": {
      "get": {
        "tags": [
          "Reference"
        ],
        "operationId": "listCurrencies",
        "security": [],
        "summary": "List supported currencies",
        "description": "Every coin and network we can swap, with the minimum amount and whether a destination memo/tag is required.",
        "responses": {
          "200": {
            "description": "Supported currencies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currencies": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Currency"
                      }
                    }
                  }
                },
                "example": {
                  "currencies": [
                    {
                      "ticker": "BTC",
                      "name": "Bitcoin",
                      "network": "BTC",
                      "hasExtraId": false,
                      "extraIdName": "",
                      "minAmount": null,
                      "image": "/icon/eb2b88067eb94d1841d5.svg"
                    },
                    {
                      "ticker": "ETH",
                      "name": "Ethereum",
                      "network": "ETH",
                      "hasExtraId": false,
                      "extraIdName": "",
                      "minAmount": null,
                      "image": "/icon/3a707c78637fc7b5354c.svg"
                    },
                    {
                      "ticker": "USDT",
                      "name": "Tether",
                      "network": "ETH",
                      "hasExtraId": false,
                      "extraIdName": "",
                      "minAmount": null,
                      "image": "/icon/db9d3021a8cfba4beaff.svg"
                    },
                    {
                      "ticker": "USDT",
                      "name": "Tether",
                      "network": "TRX",
                      "hasExtraId": false,
                      "extraIdName": "",
                      "minAmount": null,
                      "image": "/icon/db9d3021a8cfba4beaff.svg"
                    },
                    {
                      "ticker": "HBAR",
                      "name": "Hedera",
                      "network": "HBAR",
                      "hasExtraId": true,
                      "extraIdName": "Memo/Tag",
                      "minAmount": null,
                      "image": "/icon/268a5d4282686eb15608.svg"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/estimate": {
      "get": {
        "tags": [
          "Reference"
        ],
        "operationId": "getEstimate",
        "security": [],
        "summary": "Get a quote",
        "description": "Returns the best rate across compatible connected liquidity, plus the min and max for the pair, in a single call. The amount out is an estimate; the exact figure is fixed when you create the order. If the recipient needs a memo/destination tag, supply the same extraId on the quote and exchange so only routes that preserve it are considered. If the refund wallet on the send network requires a separate memo/tag, also supply refundExtraId on the quote and exchange. A supplied refund tag restricts selection to routes that preserve it; a pair may have no compatible route.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Send-coin ticker.",
            "example": "BTC"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Receive-coin ticker.",
            "example": "ETH"
          },
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "description": "Amount of the send coin.",
            "example": 0.05
          },
          {
            "name": "fromNetwork",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Network of the send coin (defaults to the coin's primary chain).",
            "example": "btc"
          },
          {
            "name": "toNetwork",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "eth"
          },
          {
            "name": "flow",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "float",
                "fixed"
              ],
              "default": "float"
            },
            "description": "`float` = live market rate on arrival (default). `fixed` = rate locked for the deposit window."
          },
          {
            "name": "extraId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "Optional recipient memo/destination tag. Surrounding whitespace is trimmed. Use the same value when creating the exchange; incompatible payout routes are excluded. XRP tags must contain ASCII digits and represent an unsigned 32-bit integer (0 through 4294967295). Leave blank for a wallet that does not require a memo."
          },
          {
            "name": "refundExtraId",
            "in": "query",
            "required": false,
            "description": "Optional refund memo/tag on the send network. Trimmed, at most 128 characters. For XRP use ASCII digits from 0 to 4294967295, including tag 0. Use the same value when creating the order so its route preserves the refund metadata.",
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "example": "0"
          }
        ],
        "responses": {
          "200": {
            "description": "A quote",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "estimate": {
                      "$ref": "#/components/schemas/Estimate"
                    }
                  }
                },
                "example": {
                  "estimate": {
                    "fromAmount": 0.05,
                    "toAmount": 1.556381,
                    "rate": 31.12762,
                    "minAmount": 0.0001651,
                    "maxAmount": null,
                    "minUsd": 1.41,
                    "fromUsd": 4331.6,
                    "toUsd": 4309.94,
                    "rateId": null,
                    "validUntil": null,
                    "warning": null,
                    "flow": "float"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request (below minimum, unsupported pair, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "this amount is below the minimum for this pair — try a larger amount",
                  "code": "below_min",
                  "detail": {
                    "payload": {
                      "range": {
                        "minAmount": 0.0001651
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/check": {
      "get": {
        "tags": [
          "Reference"
        ],
        "operationId": "checkPair",
        "security": [],
        "summary": "Check pair support",
        "description": "Side-effect-free preflight: is this pair swappable? No amount, no provider call. Handy before building an order.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "BTC"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "ETH"
          },
          {
            "name": "fromNetwork",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "toNetwork",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Whether the pair is supported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string"
                    },
                    "to": {
                      "type": "string"
                    },
                    "supported": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "from": "BTC",
                  "to": "ETH",
                  "supported": true
                }
              }
            }
          }
        }
      }
    },
    "/exchange": {
      "post": {
        "tags": [
          "Exchange"
        ],
        "operationId": "createExchange",
        "summary": "Create a swap order",
        "description": "Mints an order and returns the **deposit address** to send `from` funds to. The payout goes to `address`. Send an `Idempotency-Key` header so a retried request replays the same order instead of minting a second deposit address the customer could fund and lose.\n\nIf the send or receive coin needs a memo/destination-tag, it is returned as `payinExtraId` / expected on `extraId`. Supply refundExtraId when refundAddress needs a separate memo/tag on the send network. Use the same recipient extraId and refundExtraId values on the quote. Idempotency reservations and successful replay survive server restarts. Uncertain provider allocation returns 503 with retrySameKey:true; keep the same key and body, and do not start another order until the original outcome is resolved.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 64,
              "pattern": "^[A-Za-z0-9_-]{8,64}$"
            },
            "description": "A unique key per logical order: 8-64 ASCII letters, digits, underscores or hyphens (a UUID is suitable). Reuse the same key and intent on retries. Keys are scoped to the API owner and test/live mode. Invalid supplied keys are refused. Completed and uncertain reservations persist across restarts; an old key never silently creates another deposit address, even after its order has expired or been removed.",
            "example": "3f9c1e2a-7b41-4d9e-a1c2-order-001"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateExchange"
              },
              "example": {
                "from": "BTC",
                "to": "ETH",
                "amount": 0.05,
                "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                "refundAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
                "flow": "float"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order created (a retried idempotent request also returns 200, with `idempotent: true`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                },
                "example": {
                  "order": {
                    "id": "AKCQ8",
                    "status": "waiting",
                    "flow": "float",
                    "fromCurrency": "BTC",
                    "fromNetwork": "btc",
                    "toCurrency": "ETH",
                    "toNetwork": "eth",
                    "expectedFromAmount": 0.05,
                    "expectedToAmount": 1.55638,
                    "payinAddress": "bc1qdeposit0address0send0exactly0this0amount0to",
                    "payinExtraId": null,
                    "payinExtraIdName": "",
                    "payoutAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                    "refundAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
                    "rate": 31.1276,
                    "depositWindow": 3600,
                    "createdAt": 1790026280
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid receive address for ETH",
                  "code": "invalid_address"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Idempotency key reused with a different request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "idempotency key already used for a different request",
                  "code": "idempotency_mismatch"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Creation is still pending, its outcome is uncertain, or the local safety store is unavailable. Preserve the same Idempotency-Key and body; check the original order or contact support before starting a new order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "the order creation outcome is unknown — retry only this same request key",
                  "code": "creation_uncertain",
                  "retrySameKey": true
                }
              }
            }
          }
        }
      }
    },
    "/order/{id}": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "operationId": "getOrder",
        "summary": "Get order status",
        "description": "Poll this until `status` is terminal. Lifecycle: `waiting → confirming → exchanging → sending → finished`. Terminal-bad: `failed`, `refunded`, `expired`. Once the deposit is detected the order is never expired, even past the window.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "AKCQ8"
          }
        ],
        "responses": {
          "200": {
            "description": "Order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                },
                "example": {
                  "order": {
                    "id": "AKCQ8",
                    "status": "confirming",
                    "flow": "float",
                    "fromCurrency": "BTC",
                    "toCurrency": "ETH",
                    "expectedToAmount": 1.55638,
                    "payinHash": "5cc33ee5a44cf491cef...",
                    "payoutAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                    "depositWindow": 3600
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown order, or not owned by this key"
          }
        }
      }
    },
    "/api/portal/keys": {
      "servers": [
        {
          "url": "https://cryptoexchanger.co",
          "description": "Portal endpoints live at the site root, not under /api/v1."
        }
      ],
      "post": {
        "tags": [
          "Portal"
        ],
        "operationId": "createApiKey",
        "summary": "Create an API key (self-serve, no auth)",
        "security": [],
        "description": "Mint a new API key. Returns the plaintext `key` ONCE — store it; only its hash is kept server-side. `webhookSecret` is the secret that signs your webhooks.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mode",
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Contact email (required). A missing or malformed email returns 400 invalid_email."
                  },
                  "label": {
                    "type": "string",
                    "description": "A name for the key."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "test",
                      "live"
                    ],
                    "description": "test = sandbox (cx_test_), live = real (cx_live_)."
                  }
                }
              },
              "example": {
                "email": "dev@example.com",
                "label": "prod",
                "mode": "live"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string"
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string"
                    },
                    "webhookSecret": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "key_ab12cd34",
                  "key": "cx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                  "prefix": "cx_live_xxxxxx",
                  "label": "prod",
                  "mode": "live",
                  "webhookSecret": "whsec_..."
                }
              }
            }
          }
        }
      }
    },
    "/api/portal/keys/webhook": {
      "servers": [
        {
          "url": "https://cryptoexchanger.co"
        }
      ],
      "post": {
        "tags": [
          "Portal"
        ],
        "operationId": "setWebhook",
        "summary": "Set (or clear) your webhook URL",
        "description": "Register the HTTPS URL we POST order updates to. Requires your API key. Send an empty `url` to clear it. `signingSecret` verifies the `X-CX-Signature` header.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "HTTPS URL to receive order.updated POSTs; empty clears it."
                  }
                }
              },
              "example": {
                "url": "https://example.com/webhooks/cx"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "webhookUrl": {
                      "type": "string"
                    },
                    "signingSecret": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "id": "key_ab12cd34",
                  "webhookUrl": "https://example.com/webhooks/cx",
                  "signingSecret": "whsec_..."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "webhooks": {
    "orderUpdated": {
      "post": {
        "summary": "Order status changed",
        "description": "If you set a webhook URL on your API key, we POST the full order to it every time the status changes. Verify the `X-CX-Signature` header: `sha256=` + HMAC-SHA256(webhookSecret, rawRequestBody). Respond 2xx within ~8s; otherwise we retry periodically until it is delivered or the order is long-settled.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "example": "order.updated"
                  },
                  "order": {
                    "$ref": "#/components/schemas/Order"
                  }
                }
              },
              "example": {
                "event": "order.updated",
                "order": {
                  "id": "AKCQ8",
                  "status": "finished",
                  "payoutHash": "0xabc123..."
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cx_live_…",
        "description": "Send your key as `Authorization: Bearer cx_live_...` (or the `X-API-Key` header). Get one from the developer dashboard. Use a `cx_test_...` key for the sandbox (simulated swaps, no real funds)."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "missing or invalid API key",
              "code": "unauthorized"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "rate limit exceeded",
              "code": "rate_limited"
            }
          }
        }
      }
    },
    "schemas": {
      "Currency": {
        "type": "object",
        "properties": {
          "ticker": {
            "type": "string",
            "example": "BTC"
          },
          "name": {
            "type": "string",
            "example": "Bitcoin"
          },
          "network": {
            "type": "string",
            "description": "Chain code (uppercase), e.g. BTC, ETH, TRX, SOL. A ticker can appear on several networks.",
            "example": "BTC"
          },
          "hasExtraId": {
            "type": "boolean",
            "description": "True if a memo/destination-tag is required to receive this coin (e.g. XRP, XLM, ATOM, TON).",
            "example": false
          },
          "extraIdName": {
            "type": "string",
            "description": "Label for the memo/tag when required.",
            "example": "Memo"
          },
          "minAmount": {
            "type": "number",
            "nullable": true,
            "description": "Usually null in the bulk catalogue (minimums depend on live network fees). Call GET /estimate for the authoritative minimum, or read it from a below_min error.",
            "example": null
          },
          "image": {
            "type": "string",
            "description": "Relative, content-hashed icon URL (e.g. /icon/<hash>.svg). Prefix with the site origin to load it.",
            "example": "/icon/eb2b88067eb94d1841d5.svg"
          }
        }
      },
      "Estimate": {
        "type": "object",
        "properties": {
          "fromAmount": {
            "type": "number",
            "example": 0.05
          },
          "toAmount": {
            "type": "number",
            "description": "Estimated amount received.",
            "example": 1.556381
          },
          "rate": {
            "type": "number",
            "description": "toAmount per 1 fromCoin.",
            "example": 31.12762
          },
          "minAmount": {
            "type": "number",
            "nullable": true,
            "example": 0.0004
          },
          "maxAmount": {
            "type": "number",
            "nullable": true,
            "description": "null = no upper limit.",
            "example": null
          },
          "fromUsd": {
            "type": "number",
            "nullable": true,
            "example": 4331.6
          },
          "toUsd": {
            "type": "number",
            "nullable": true,
            "example": 4309.94
          },
          "flow": {
            "type": "string",
            "enum": [
              "float",
              "fixed"
            ],
            "example": "float"
          },
          "minUsd": {
            "type": "number",
            "nullable": true,
            "description": "USD value of minAmount.",
            "example": 1.41
          },
          "rateId": {
            "type": "string",
            "nullable": true,
            "description": "Opaque quote id for flow=fixed; pass it back as rateId on POST /exchange to lock exactly this quote. null on float.",
            "example": null
          },
          "validUntil": {
            "type": "number",
            "nullable": true,
            "description": "Unix seconds this locked quote is valid until (flow=fixed). null on float.",
            "example": null
          },
          "warning": {
            "type": "string",
            "nullable": true,
            "description": "A non-blocking advisory about this quote, or null.",
            "example": null
          }
        }
      },
      "CreateExchange": {
        "type": "object",
        "required": [
          "from",
          "to",
          "amount",
          "address",
          "refundAddress"
        ],
        "properties": {
          "from": {
            "type": "string",
            "description": "Send-coin ticker.",
            "example": "BTC"
          },
          "to": {
            "type": "string",
            "description": "Receive-coin ticker.",
            "example": "ETH"
          },
          "amount": {
            "type": "number",
            "description": "Amount of the send coin.",
            "example": 0.05
          },
          "address": {
            "type": "string",
            "description": "Recipient address (where the swapped coins are sent).",
            "example": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
          },
          "refundAddress": {
            "type": "string",
            "description": "Refund wallet address on the send network. If this wallet requires a separate memo/tag, supply refundExtraId as well; otherwise it may be unable to receive a refund.",
            "example": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
          },
          "fromNetwork": {
            "type": "string",
            "example": "btc"
          },
          "toNetwork": {
            "type": "string",
            "example": "eth"
          },
          "extraId": {
            "type": "string",
            "maxLength": 128,
            "description": "Memo/destination-tag for the RECIPIENT, when the receive coin's hasExtraId is true. Supply the same value on the estimate request so the quote uses a compatible route. Surrounding whitespace is trimmed. XRP tags must be ASCII digits from 0 through 4294967295. The order is still created without it, but a payout to an address that needs a tag can be unrecoverable — always collect it for memo coins."
          },
          "flow": {
            "type": "string",
            "enum": [
              "float",
              "fixed"
            ],
            "default": "float"
          },
          "rateId": {
            "type": "string",
            "description": "For flow=fixed only: the rateId from a prior GET /estimate, to lock that exact quote."
          },
          "refundExtraId": {
            "type": "string",
            "maxLength": 128,
            "example": "0",
            "description": "Optional refund memo/tag for refundAddress on the send network. Trimmed. For XRP use ASCII uint32 digits (0 through 4294967295). Supply the same value in /estimate. Only compatible routes are eligible; unsupported routes fail before allocation."
          }
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "AKCQ8"
          },
          "status": {
            "type": "string",
            "enum": [
              "waiting",
              "confirming",
              "exchanging",
              "sending",
              "finished",
              "failed",
              "refunded",
              "expired"
            ],
            "example": "waiting"
          },
          "flow": {
            "type": "string",
            "enum": [
              "float",
              "fixed"
            ],
            "example": "float"
          },
          "fromCurrency": {
            "type": "string",
            "example": "BTC"
          },
          "fromNetwork": {
            "type": "string",
            "example": "btc"
          },
          "toCurrency": {
            "type": "string",
            "example": "ETH"
          },
          "toNetwork": {
            "type": "string",
            "example": "eth"
          },
          "expectedFromAmount": {
            "type": "number",
            "description": "Send exactly this amount.",
            "example": 0.05
          },
          "expectedToAmount": {
            "type": "number",
            "example": 1.55638
          },
          "payinAddress": {
            "type": "string",
            "description": "Deposit address — send the send-coin here.",
            "example": "bc1qdeposit..."
          },
          "payinExtraId": {
            "type": "string",
            "nullable": true,
            "description": "Memo/tag to include with your deposit, if required."
          },
          "payinExtraIdName": {
            "type": "string",
            "example": ""
          },
          "payoutAddress": {
            "type": "string",
            "example": "0x742d35Cc..."
          },
          "refundAddress": {
            "type": "string",
            "nullable": true
          },
          "rate": {
            "type": "number",
            "example": 31.1276
          },
          "fromUsd": {
            "type": "number",
            "nullable": true
          },
          "toUsd": {
            "type": "number",
            "nullable": true
          },
          "depositWindow": {
            "type": "number",
            "nullable": true,
            "description": "Seconds from createdAt within which to deposit. Deposit inside this window and you're guaranteed; a funded order is never expired.",
            "example": 3600
          },
          "createdAt": {
            "type": "number",
            "description": "Unix seconds (may be fractional).",
            "example": 1790026280
          },
          "payinHash": {
            "type": "string",
            "nullable": true,
            "description": "Your deposit tx hash, once detected."
          },
          "payoutHash": {
            "type": "string",
            "nullable": true,
            "description": "The payout tx hash, once sent."
          },
          "payoutExtraId": {
            "type": "string",
            "nullable": true,
            "description": "The destination memo/tag you supplied as extraId, echoed back (null if none)."
          },
          "amountFrom": {
            "type": "number",
            "nullable": true,
            "description": "Actual deposit amount received, once detected."
          },
          "amountTo": {
            "type": "number",
            "nullable": true,
            "description": "Actual amount paid out, once sent."
          },
          "finishedAt": {
            "type": "number",
            "nullable": true,
            "description": "Unix seconds the order reached a terminal state."
          },
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "demo"
            ],
            "description": "demo = a sandbox (cx_test_) order: simulated, no real funds.",
            "example": "live"
          },
          "warning": {
            "type": "string",
            "nullable": true,
            "description": "A non-blocking advisory, e.g. the destination coin needs a memo/tag but no extraId was supplied."
          },
          "depositAt": {
            "type": "number",
            "nullable": true,
            "description": "Unix seconds the deposit was detected (null until then)."
          },
          "refundExtraId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The supplied refund memo/tag, preserved with the order (null or absent if none)."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message.",
            "example": "amount is below the minimum for this pair"
          },
          "code": {
            "type": "string",
            "description": "Stable machine code.",
            "enum": [
              "below_min",
              "above_max",
              "unsupported_currency",
              "unsupported_network",
              "network_required",
              "invalid_address",
              "invalid_request",
              "no_route",
              "idempotency_mismatch",
              "unauthorized",
              "rate_limited",
              "not_found",
              "internal",
              "invalid_extra_id",
              "invalid_refund_extra_id",
              "invalid_idempotency_key",
              "creation_pending",
              "creation_uncertain",
              "creation_safety_unavailable",
              "refund_memo_unsupported"
            ],
            "example": "below_min"
          },
          "networks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present on network_required / unsupported_network: the valid networks to choose from."
          },
          "detail": {
            "type": "object",
            "nullable": true,
            "description": "Present on below_min / above_max when the provider returns a range: {payload:{range:{minAmount,maxAmount}}}."
          },
          "retrySameKey": {
            "type": "boolean",
            "description": "When true, retry only the same logical request with the same Idempotency-Key. An allocation may already exist; do not generate a new key."
          }
        }
      }
    }
  }
}
