{
  "openapi": "3.1.1",
  "info": {
    "title": "ChangeNOW exchange API",
    "version": "1.0.0",
    "description": "Machine-readable description of the anonymous ChangeNOW exchange API.\n\nEvery endpoint here is anonymous: no API key, no account, no header, no cookie, no signature. There are five, and together they cover the whole job.\n\n  GET  /v3/exchange/estimate   quote an exchange\n  POST /v3/transactions        create the order and get the deposit address\n  GET  /v3/transactions/{id}   read the state of that order\n  GET  /v1/assets              the catalogue: what can be exchanged\n  GET  /v1/assets/{asset}      one asset in detail\n\nStart at GET /v1/assets. It is the only place asset identifiers come from, and the three /v3 calls take the identifiers it hands out. Then quote, then create, then read the state until it settles.\n\nAn asset carries three identifiers and they are not interchangeable. The canonical id <ticker>.<network> is what the /v3 calls take, split into two separate parameters: usdt with trx, never usdttrc20. The legacy ticker belongs only in the web link https://changenow.io/exchange?from=<ticker>&to=<ticker>. The page slug addresses https://changenow.io/currencies/<slug>. Passing one where another is expected quotes a different pair and still answers 200, so take both identifiers from the catalogue rather than building either one.\n\nA refusal from a /v3 endpoint is a JSON object keyed by the parameter that has to change, with a machine code, an English message, the expected value and, where the set is finite, the allowed values. 4xx means the request has to change; 5xx means it does not and a retry is the right move. Each /v3 endpoint also serves its own manual in plain text - ?help on the two GET calls, {\"help\": true} in the body on the POST - and that manual is the newer authority if it ever disagrees with this document.\n\nNothing in this API takes custody of funds. An exchange is committed only when the person sends the deposit to the payinAddress that POST /v3/transactions returned, and no endpoint here moves money on its own.\n\nEarlier versions of the exchange calls - /v1.3/exchange/estimate, /v1.1/transactions and /transactions/{id} - are what the website itself uses. They still answer and they are deliberately not described here: they report a failure as one bare message, so a caller cannot tell which parameter to fix. Anything written against them belongs on /v3."
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "servers": [
    {
      "url": "https://vip-api.changenow.io",
      "description": "Prod"
    }
  ],
  "tags": [
    {
      "name": "Exchange",
      "description": "Quoting an exchange: the rate, both amounts, and the limits of the pair."
    },
    {
      "name": "Transactions",
      "description": "Creating an exchange order and reading its state. Creating one moves no money."
    },
    {
      "name": "Assets",
      "description": "Asset catalogue for agents: the list of currencies that can be exchanged, and the detail of one currency."
    }
  ],
  "paths": {
    "/v3/exchange/estimate": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "operationId": "estimateExchangeV3",
        "summary": "Quote an exchange",
        "description": "Anonymous. Answers one quote and nothing else.\n\nAssets are passed per side, as the canonical ticker and its network separately: fromCurrency=usdt with fromNetwork=trx, never usdttrc20. Both identifiers are in every row of GET /v1/assets. A legacy ticker is accepted here and answered in canonical form, so the answer always names the pair the way the other version 3 endpoints expect it.\n\nPass exactly one amount. fromAmount is the amount being sold and is the default. toAmount is the amount to receive and needs type=reverse, which is only quoted on a locked rate, so type=reverse always goes together with flow=fixed-rate. Passing both amounts is refused, and the refusal says which one to drop.\n\nminAmount and maxAmount belong to the pair and not to the asset, and they move with the rate, so read them from this answer instead of caching them. limitsUnit is the currency they are counted in: fromCurrency for type=direct, toCurrency for type=reverse. An amount outside those limits is refused rather than quoted, and the refusal carries min or max with the same unit.\n\nflow=fixed-rate locks the rate and returns rateId with validUntil. A rateId holds for about two minutes: create the order right after the quote, and quote again if the person needed time to think. Whether a pair can hold a fixed rate is decided by this answer and not by any flag in the asset catalogue. useRateId=false prices a fixed rate without locking it.\n\nprovider names who would execute the order. changenow is this service; third-party means a partner aggregator carries the order out under its own rate and its own support, and that has to be shown to the person before they send funds.\n\nAn unknown parameter is refused with 400 rather than dropped, so a quote that answered 200 used everything that was sent. The one exception is tracking and client parameters such as utm_*, gclid, fbclid, api_key, ref, lang and cache busters: those are dropped and listed back by name in the ignored field of the answer, and their values are never echoed.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "fromCurrency",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "Canonical ticker of the asset being sold, for example btc or usdt. Listed by GET /v1/assets."
          },
          {
            "name": "fromNetwork",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "Network the asset being sold lives on, for example btc or trx. The same ticker exists on several networks and they are different assets."
          },
          {
            "name": "toCurrency",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "Canonical ticker of the asset being bought."
          },
          {
            "name": "toNetwork",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "Network the asset being bought lives on."
          },
          {
            "name": "fromAmount",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DecimalAmountV3"
            },
            "description": "Amount being sold, counted in fromCurrency. Required unless type=reverse. Mutually exclusive with toAmount."
          },
          {
            "name": "toAmount",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DecimalAmountV3"
            },
            "description": "Amount to receive, counted in toCurrency. Required for type=reverse, which also needs flow=fixed-rate. Mutually exclusive with fromAmount."
          },
          {
            "name": "flow",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EstimateFlow"
            },
            "description": "standard leaves the rate floating until the deposit arrives. fixed-rate locks it and returns rateId."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EstimateDirection"
            },
            "description": "direct names the amount to send, reverse names the amount to receive. reverse requires flow=fixed-rate."
          },
          {
            "name": "useRateId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Defaults to true for flow=fixed-rate, which is what returns rateId. Pass false to price a fixed rate without locking it. It cannot be used with flow=standard."
          },
          {
            "name": "promoCode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "The code the person holds. A valid one changes the amount, so quote with the same code the order will carry."
          },
          {
            "name": "help",
            "in": "query",
            "required": false,
            "allowEmptyValue": true,
            "schema": {
              "type": "boolean"
            },
            "description": "Bare flag. Answers the manual of the version 3 endpoints as text/plain and performs no other work."
          }
        ],
        "responses": {
          "200": {
            "description": "The quote, or the manual when help was passed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimateResponseV3"
                },
                "example": {
                  "quote": {
                    "fromCurrency": "btc",
                    "fromNetwork": "btc",
                    "toCurrency": "eth",
                    "toNetwork": "eth",
                    "fromAmount": "0.01",
                    "toAmount": "0.3105365",
                    "rate": "31.05365",
                    "flow": "fixed-rate",
                    "type": "direct",
                    "rateId": "60f5772c8285ca0f7e49576e:f496de75-64fe-4ba1-95cb-da1fac4cf75c",
                    "validUntil": "2026-09-04T14:30:41.701Z",
                    "minAmount": "0.001",
                    "maxAmount": "780",
                    "limitsUnit": "btc",
                    "provider": "third-party",
                    "highNetworkFee": false,
                    "speedForecastMinutes": "10-60",
                    "warning": null,
                    "quoteId": "phvsgQECxQ4v-NqUufJ6R"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "GET /v3/exchange/estimate - quote an exchange.\n"
              }
            }
          },
          "400": {
            "description": "The request has to change. The body names the parameter at fault: a missing or unknown one, an amount outside the limits of the pair, a pair no provider exchanges, or a fixed rate that is not available right now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                },
                "examples": {
                  "amountBelowMinimum": {
                    "summary": "The amount is under the pair minimum",
                    "value": {
                      "status": 400,
                      "errors": {
                        "fromAmount": {
                          "code": "AMOUNT_OUT_OF_RANGE",
                          "message": "fromAmount is below the minimum for this pair. The minimum is 0.0000163 btc.",
                          "expected": "a decimal amount greater than zero",
                          "min": "0.0000163",
                          "unit": "btc"
                        }
                      },
                      "help": "GET /v3/exchange/estimate?help",
                      "upstreamResponse": {
                        "code": "deposit_too_small",
                        "message": "Out of min amount"
                      }
                    }
                  },
                  "unknownParameter": {
                    "summary": "A parameter this endpoint does not have",
                    "value": {
                      "status": 400,
                      "errors": {
                        "amount": {
                          "code": "UNKNOWN_PARAMETER",
                          "message": "amount is not a parameter of this endpoint. The amount is fromAmount, or toAmount with type=reverse.",
                          "expected": "no value",
                          "allowed": [
                            "fromCurrency",
                            "fromNetwork",
                            "toCurrency",
                            "toNetwork",
                            "fromAmount",
                            "toAmount",
                            "flow",
                            "type",
                            "useRateId",
                            "promoCode",
                            "help"
                          ]
                        }
                      },
                      "help": "GET /v3/exchange/estimate?help"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, code RATE_LIMITED. The request itself is fine: wait and repeat it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "500": {
            "description": "The request failed for a reason that is not in its parameters, code INTERNAL_ERROR. Nothing has to change: retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "503": {
            "description": "The quoting service did not answer, code UPSTREAM_UNAVAILABLE or RATE_UNAVAILABLE. Nothing in the request has to change: retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          }
        }
      }
    },
    "/v3/transactions": {
      "post": {
        "tags": [
          "Transactions"
        ],
        "operationId": "createTransactionV3",
        "summary": "Create an exchange order",
        "description": "Anonymous. Creates the order and answers with the deposit address.\n\nCreating an order moves no money and commits nothing. The exchange starts only when the person sends the amount to payinAddress, and only that address may be shown to them: never a modified one and never one from another source.\n\nThe pair, the amount, flow and type follow the same rules as GET /v3/exchange/estimate, and the sequence to call them in is quote first, then create with the same parameters.\n\naddress is the recipient address and must belong to toCurrency on toNetwork: a payout sent on the wrong network is lost. The check applied to it is a format pattern, not a checksum, so an address with one mistyped character can pass it; have the person confirm the address they gave. When the service holds no pattern for that asset at all the address is accepted unchecked, addressFormatChecked comes back false and addressWarning says so in words.\n\nextraId is the memo or destination tag and belongs only to assets that use one; on any other asset it is refused rather than ignored. refundAddress is where the funds go back if the order cannot be completed, and a refund travels the network the deposit came from, so it must belong to fromCurrency on fromNetwork.\n\nrateId is required for flow=fixed-rate and for type=reverse, because only a locked quote can hold the amount that was asked for. It comes from GET /v3/exchange/estimate and holds for about two minutes; an expired one is refused on the rateId field with code RATE_UNAVAILABLE, and the answer is to quote again.\n\nprovider carries the same meaning as in a quote and the same duty to relay it. validUntil is present when the rate is locked and is how long the person has to send the deposit at that rate, set by whoever executes the order; read the field rather than assuming a number.\n\nUnknown parameters are refused with 400, with the same exception for tracking and client parameters, which come back by name in ignored.",
        "security": [
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTransactionRequestV3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The manual of the version 3 endpoints, answered when help is passed.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "GET /v3/exchange/estimate - quote an exchange.\n"
              }
            }
          },
          "201": {
            "description": "The order was created. Nothing is committed until the deposit reaches payinAddress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateTransactionResponseV3"
                },
                "example": {
                  "id": "041c0026e6e482",
                  "provider": "changenow",
                  "fromCurrency": "usdt",
                  "fromNetwork": "trx",
                  "toCurrency": "eth",
                  "toNetwork": "eth",
                  "fromAmount": "100",
                  "toAmount": "0.0404023",
                  "flow": "standard",
                  "type": "direct",
                  "payinAddress": "TReRNuK194Y193LRCBgR3QozyteFAzauEw",
                  "payinExtraId": null,
                  "payoutAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
                  "payoutExtraId": null,
                  "refundAddress": "TKLN16kJTw81oUsztvDq3pPBo2rDsQvWxS",
                  "refundExtraId": null,
                  "addressFormatChecked": true
                }
              }
            }
          },
          "400": {
            "description": "The order has to change. The body names the parameter at fault: the recipient address, the memo, an amount outside the limits of the pair, a missing or expired rateId, or a pair for which no provider creates an order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                },
                "examples": {
                  "invalidAddress": {
                    "summary": "The address does not belong to the asset and network being bought",
                    "value": {
                      "status": 400,
                      "errors": {
                        "address": {
                          "code": "INVALID_ADDRESS",
                          "message": "address is not a valid eth address on network eth.",
                          "expected": "an address of eth on network eth"
                        }
                      },
                      "help": "GET /v3/exchange/estimate?help"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, code RATE_LIMITED. No order was created: wait and repeat the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "500": {
            "description": "The order was not created for a reason that is not in the request, code INTERNAL_ERROR. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "503": {
            "description": "The exchange service did not create the order and the failure is not in this request, code UPSTREAM_UNAVAILABLE. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          }
        }
      }
    },
    "/v3/transactions/{id}": {
      "get": {
        "tags": [
          "Transactions"
        ],
        "operationId": "getTransactionV3",
        "summary": "Read the status of an exchange order",
        "description": "Anonymous. Reads one order by the id that POST /v3/transactions returned.\n\nThe answer holds the order and nothing about the web application: the status, the executor, the pair in canonical and in legacy form, the expected and the received amounts, both addresses with their memos, the refund fields, the deposit, payout and refund hashes, validUntil and the timestamps. Every amount is a decimal string.\n\nexpectedAmountFrom and expectedAmountTo are what the order was created for. amountFrom is what actually arrived and stays null until the deposit is seen, which is the field to read when answering whether the person has paid, together with depositReceivedAt. amountTo is not the counterpart of it: it is filled before any payout happens, so payoutHash is what proves the funds went out.\n\nprovider carries the same meaning as in a quote: changenow, or third-party when a partner aggregator holds the order. It decides who the person has to be sent to for support, so it is relayed rather than hidden.\n\nOnly an id of at least fourteen characters of lowercase letters and digits reaches this endpoint. Any other shape - uppercase, a hyphen, an underscore, or fewer than fourteen characters - is answered at the edge with a bare {\"statusCode\": 404, \"message\": \"Cannot GET ...\", \"error\": \"Not Found\"} instead of the error object of this API. That answer means the id is malformed. It does not mean the order is missing and it does not mean this endpoint is undeployed, so it must never be relayed to a person as advice to abandon an order: check the id and repeat.\n\nA ChangeNOW order id is fourteen characters; an order held by a partner aggregator is twenty-four.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/OrderIdV3"
            },
            "description": "The id that POST /v3/transactions returned. Fourteen characters for a ChangeNOW order, twenty-four for one held by a partner aggregator."
          },
          {
            "name": "subId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Identifier of a sub-record of an order that was carried out in several steps. It selects which record is read and is not repeated in the answer, so pass it only when something already handed you one."
          },
          {
            "name": "help",
            "in": "query",
            "required": false,
            "allowEmptyValue": true,
            "schema": {
              "type": "boolean"
            },
            "description": "Bare flag. Answers the manual of the version 3 endpoints as text/plain and performs no other work."
          }
        ],
        "responses": {
          "200": {
            "description": "The order, or the manual when help was passed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransactionStatusV3"
                },
                "example": {
                  "id": "041c0026e6e482",
                  "status": "waiting",
                  "provider": "changenow",
                  "fromCurrency": "usdt",
                  "fromNetwork": "trx",
                  "toCurrency": "eth",
                  "toNetwork": "eth",
                  "fromLegacyTicker": "usdttrc20",
                  "toLegacyTicker": "eth",
                  "expectedAmountFrom": "100",
                  "expectedAmountTo": "0.0404023",
                  "amountFrom": null,
                  "amountTo": "0.0404023",
                  "payinAddress": "TReRNuK194Y193LRCBgR3QozyteFAzauEw",
                  "payinExtraId": null,
                  "payoutAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
                  "payoutExtraId": null,
                  "refundAddress": "TKLN16kJTw81oUsztvDq3pPBo2rDsQvWxS",
                  "refundExtraId": null,
                  "payinHash": null,
                  "payoutHash": null,
                  "refundHash": null,
                  "refundAmount": null,
                  "validUntil": null,
                  "depositReceivedAt": null,
                  "createdAt": "2026-09-04T14:51:15.666Z",
                  "updatedAt": "2026-09-04T14:51:15.666Z"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "GET /v3/exchange/estimate - quote an exchange.\n"
              }
            }
          },
          "400": {
            "description": "The id is not shaped like an order id, code INVALID_PARAMETER, or a query parameter is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "404": {
            "description": "Two different answers arrive with this code and they mean different things. The error object of this API, keyed on id with code NOT_FOUND, means no order has this id. A bare statusCode / message / error body means the id was malformed and never reached the endpoint: check the id and repeat, do not tell the person the order is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ApiErrorV3"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "noSuchOrder": {
                    "summary": "No order with this id",
                    "value": {
                      "status": 404,
                      "errors": {
                        "id": {
                          "code": "NOT_FOUND",
                          "message": "No order with this id. Check the id returned when the order was created; an order created seconds ago may not be readable yet.",
                          "expected": "the id returned when the order was created"
                        }
                      },
                      "help": "GET /v3/exchange/estimate?help"
                    }
                  },
                  "malformedId": {
                    "summary": "The id was rejected at the edge for its shape",
                    "value": {
                      "statusCode": 404,
                      "message": "Cannot GET /v3/transactions/0936D614dc32b2",
                      "error": "Not Found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, code RATE_LIMITED. The order is not affected: wait and repeat the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "500": {
            "description": "The order could not be read for a reason that is not in the request, code INTERNAL_ERROR. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          },
          "503": {
            "description": "The service that holds the order did not answer, code UPSTREAM_UNAVAILABLE. The order is not affected: retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorV3"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets": {
      "get": {
        "tags": [
          "Assets"
        ],
        "operationId": "getAssets",
        "summary": "List the assets that can be exchanged",
        "description": "Anonymous, read only, rebuilt every minute.\n\nEvery asset carries three different identifiers and they are not interchangeable: `asset` is the canonical id `<ticker>.<network>` (split it on the dot for GET /v3/exchange/estimate and POST /v3/transactions), `ticker` is the legacy ticker and belongs only in the web link `https://changenow.io/exchange?from=<ticker>&to=<ticker>`, and the page slug (see the detail call) addresses `https://changenow.io/currencies/<slug>`. Both the canonical id and the legacy ticker are in every row, so neither has to be constructed.\n\nformat=json answers a page: `total` and `has_more` are in every response, and `summary` says in one sentence whether the list is complete. format=csv answers the complete selection in one response, RFC 4180, four columns, flags as letters (F=fiat, R=fixed_rate, M=memo) - the whole catalogue is around 40 KB that way, roughly a quarter of the json form.\n\nAn unknown query parameter is refused with 400 rather than dropped, so a filter that answered 200 was applied. Minimum and maximum amounts and the rate are not asset properties: they depend on the pair and come from GET /v3/exchange/estimate.\n\nParameters that cannot change the selection are accepted rather than refused: utm_* and the other click ids, ref, referrer, api_key, key, token, lang, locale, callback, pretty and cache busters such as _ or ts. The ones that were present come back by name in `ignored` and in `summary`; their values are never echoed. Any other unknown parameter is refused with 400 and answered with the parameter that does the job: page, offset, skip and start point at cursor and limit, per_page and size point at limit, search and query point at q, sort points at the fixed order, fields and include point at the detail call, and a near miss such as nework is answered with \"Did you mean network?\".",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Substring match over the canonical id, the legacy ticker, the page slug, the name and the network. Case insensitive."
          },
          {
            "name": "network",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "Exact network match, for example trx, eth, sol."
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/AssetKind"
            },
            "description": "Asset kind. Defaults to crypto. Fiat can only be the source of a purchase."
          },
          {
            "name": "fixed_rate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Keep only assets that do (true) or do not (false) support a fixed rate. Accepts a bare flag, 1/0 and true/false."
          },
          {
            "name": "popular",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Keep only popular assets. Accepts a bare flag, 1/0 and true/false."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            },
            "description": "Page size. Applies to format=json only; passing it with format=csv is refused with 400, because csv always returns the complete selection."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 256
            },
            "description": "Opaque continuation token taken from next_cursor of a previous call. It belongs to the filters it was issued for: reusing it after changing a filter, or passing a broken one, is refused with 400 and never silently returns the first page. Refused with format=csv."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/AssetFormat"
            },
            "description": "Response format. json returns an envelope with counts and a cursor, csv returns the complete selection as a table."
          },
          {
            "name": "help",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Return the plain text manual for this endpoint instead of data: parameters, the flag legend, the three identifiers and examples."
          }
        ],
        "responses": {
          "200": {
            "description": "The matching assets, as an envelope (format=json), a table (format=csv) or the manual (help).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetListEnvelope"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "asset,ticker,name,flags[F=fiat;R=fixed_rate;M=memo]\nbtc.btc,btc,Bitcoin,R\nusdt.trx,usdttrc20,Tether USD (TRON),R\n"
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "GET /v1/assets - the ChangeNOW asset catalogue for agents and scripts.\n"
              }
            }
          },
          "400": {
            "description": "A parameter is unknown, out of range, or not supported in this combination. The body names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetsFieldError"
                }
              }
            }
          },
          "503": {
            "description": "The catalogue is not built yet. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetsError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "operationId": "getAsset",
        "summary": "Get one asset in detail",
        "description": "Accepts any of the three identifiers: the canonical id `usdt.trx`, the legacy ticker `usdttrc20` or the page slug `tether-trc20`. Adds the fields needed to validate an address and to warn a person before they send funds.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Canonical id, legacy ticker or page slug.",
            "example": "usdt.trx"
          }
        ],
        "responses": {
          "200": {
            "description": "The asset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetDetail"
                }
              }
            }
          },
          "404": {
            "description": "No asset matches the identifier. The body says how to search for the right one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetsError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "EstimateFlow": {
        "type": "string",
        "enum": [
          "standard",
          "fixed-rate"
        ],
        "default": "standard"
      },
      "EstimateDirection": {
        "type": "string",
        "enum": [
          "direct",
          "reverse"
        ],
        "default": "direct"
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "statusCode": {
            "type": "integer"
          },
          "message": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "error": {
            "type": "string"
          }
        }
      },
      "AssetKind": {
        "type": "string",
        "enum": [
          "crypto",
          "fiat",
          "all"
        ],
        "description": "crypto and fiat filter the catalogue, all removes the filter. The kind of a single asset is either crypto or fiat."
      },
      "AssetFormat": {
        "type": "string",
        "enum": [
          "json",
          "csv"
        ],
        "default": "json"
      },
      "AssetListItem": {
        "type": "object",
        "required": [
          "asset",
          "ticker",
          "name",
          "network",
          "kind",
          "fixed_rate",
          "memo"
        ],
        "properties": {
          "asset": {
            "type": "string",
            "example": "usdt.trx",
            "description": "Canonical id `<ticker>.<network>`. Split it on the dot for GET /v3/exchange/estimate and POST /v3/transactions."
          },
          "ticker": {
            "type": "string",
            "example": "usdttrc20",
            "description": "Legacy ticker. Use it only in the web link https://changenow.io/exchange?from=usdttrc20."
          },
          "name": {
            "type": "string",
            "example": "Tether USD (TRON)",
            "description": "Human readable name, for showing to a person."
          },
          "network": {
            "type": "string",
            "example": "trx",
            "description": "Network, also the part of the canonical id after the dot."
          },
          "kind": {
            "type": "string",
            "enum": [
              "crypto",
              "fiat"
            ]
          },
          "fixed_rate": {
            "type": "boolean",
            "description": "Whether a fixed rate swap can be created for this asset at all."
          },
          "memo": {
            "type": "boolean",
            "description": "Whether a memo or destination tag has to be collected together with the address."
          }
        }
      },
      "AssetDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AssetListItem"
          },
          {
            "type": "object",
            "properties": {
              "slug": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "tether-trc20",
                "description": "Page address https://changenow.io/currencies/<slug>."
              },
              "decimals": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "memo_name": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Destination Tag",
                "description": "What the memo is called for this asset, to ask a person for it by name."
              },
              "address_regex": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Address format, for validating input before creating a swap."
              },
              "memo_regex": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "token_contract": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "warning_send": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Warning to show before a person sends this asset."
              },
              "warning_receive": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "explorer_address_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "explorer_tx_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "price_usd": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Indicative price, not the exchange rate. The rate comes from GET /v3/exchange/estimate."
              }
            }
          }
        ]
      },
      "AssetListEnvelope": {
        "type": "object",
        "required": [
          "version",
          "returned",
          "total",
          "has_more",
          "next_cursor",
          "summary",
          "assets"
        ],
        "properties": {
          "version": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the catalogue was built."
          },
          "returned": {
            "type": "integer",
            "description": "Assets in this response."
          },
          "total": {
            "type": "integer",
            "description": "Assets matching the filters."
          },
          "has_more": {
            "type": "boolean",
            "description": "false means this response holds everything that matched."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass it back as cursor to continue. null when has_more is false."
          },
          "summary": {
            "type": "string",
            "description": "The same in one sentence, including whether the list is complete."
          },
          "assets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssetListItem"
            }
          },
          "ignored": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Parameters that were present in the request, could not change the selection and were ignored: tracking, auth and cache busting names. Absent when there were none. Names only, never values.",
            "example": [
              "gclid",
              "utm_source"
            ]
          }
        }
      },
      "AssetsFieldError": {
        "type": "object",
        "properties": {
          "status": {
            "type": "integer",
            "example": 400
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Field name to the reason, so the call can be corrected without guessing.",
            "example": {
              "page": "page is not supported, and unknown parameters are refused rather than ignored. Pages are taken with cursor and limit, or drop paging entirely with format=csv."
            }
          },
          "help": {
            "type": "string",
            "example": "GET /v1/assets?help",
            "description": "Where the manual for this endpoint is."
          }
        }
      },
      "AssetsError": {
        "type": "object",
        "properties": {
          "status": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          },
          "help": {
            "type": "string",
            "example": "GET /v1/assets?help"
          }
        }
      },
      "DecimalAmountV3": {
        "description": "A positive decimal amount. Accepted as a string or as a number; answered as a string, so read it as text and never through a binary float.",
        "oneOf": [
          {
            "type": "string",
            "pattern": "^[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$"
          },
          {
            "type": "number",
            "exclusiveMinimum": 0
          }
        ]
      },
      "OrderIdV3": {
        "type": "string",
        "pattern": "^[a-z0-9]{14,}$",
        "description": "Identifier of an order: at least fourteen characters of lowercase letters and digits. Fourteen for a ChangeNOW order, twenty-four for one held by a partner aggregator. Any other shape is rejected at the edge before it reaches the endpoint."
      },
      "SwapProviderV3": {
        "type": "string",
        "enum": [
          "changenow",
          "third-party"
        ],
        "description": "Who executes the order. changenow is this service. third-party means a partner aggregator carries it out under its own rate and its own support, which has to be shown to the person before they send funds."
      },
      "IgnoredParametersV3": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Names of the tracking and client parameters that were sent and dropped, for example utm_source, gclid or api_key. Present only when there was at least one. Values are never echoed. Any other unknown parameter is refused with 400 instead of appearing here."
      },
      "EstimateQuoteV3": {
        "type": "object",
        "description": "One quote for one pair and one amount.",
        "required": [
          "fromCurrency",
          "fromNetwork",
          "toCurrency",
          "toNetwork",
          "fromAmount",
          "toAmount",
          "rate",
          "flow",
          "type",
          "rateId",
          "validUntil",
          "minAmount",
          "maxAmount",
          "limitsUnit",
          "provider",
          "highNetworkFee",
          "speedForecastMinutes",
          "warning",
          "quoteId"
        ],
        "properties": {
          "fromCurrency": {
            "type": "string",
            "description": "Canonical ticker of the asset being sold, as it was resolved. A legacy ticker that was sent comes back in canonical form here."
          },
          "fromNetwork": {
            "type": "string",
            "description": "Network of the asset being sold."
          },
          "toCurrency": {
            "type": "string",
            "description": "Canonical ticker of the asset being bought."
          },
          "toNetwork": {
            "type": "string",
            "description": "Network of the asset being bought."
          },
          "fromAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount to send, counted in fromCurrency. Filled on both directions: it is the amount that was asked for on type=direct and the amount computed on type=reverse."
          },
          "toAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount to receive, counted in toCurrency. Filled on both directions."
          },
          "rate": {
            "type": [
              "string",
              "null"
            ],
            "description": "toAmount divided by fromAmount."
          },
          "flow": {
            "$ref": "#/components/schemas/EstimateFlow"
          },
          "type": {
            "$ref": "#/components/schemas/EstimateDirection"
          },
          "rateId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Handle of the locked rate, to be passed to POST /v3/transactions. Present only when the rate is locked, null otherwise."
          },
          "validUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the locked rate expires, about two minutes out. This is the life of rateId and not the deposit window: the deposit window is the validUntil of the created order and is a different, longer value."
          },
          "minAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Smallest amount this pair accepts, counted in limitsUnit. null when the executor named none. It belongs to the pair and moves with the rate."
          },
          "maxAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Largest amount this pair accepts, counted in limitsUnit. null when there is no ceiling."
          },
          "limitsUnit": {
            "type": "string",
            "description": "Currency minAmount and maxAmount are counted in: fromCurrency for type=direct, toCurrency for type=reverse."
          },
          "provider": {
            "$ref": "#/components/schemas/SwapProviderV3"
          },
          "highNetworkFee": {
            "type": "boolean",
            "description": "true when the network fee eats a large part of the amount. Worth relaying: a smaller amount on this pair is poor value."
          },
          "speedForecastMinutes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rough completion time in minutes, as a range such as 10-60. A forecast, not a commitment."
          },
          "warning": {
            "type": [
              "string",
              "null"
            ],
            "description": "A warning about this pair or amount that has to be relayed to the person as it is. null when there is none."
          },
          "quoteId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Handle of this answer, for quoting to support. It is not a rateId and cannot create an order."
          }
        }
      },
      "EstimateResponseV3": {
        "type": "object",
        "description": "The answer of GET /v3/exchange/estimate: one quote, plus the names of any tracking parameters that were dropped.",
        "required": [
          "quote"
        ],
        "properties": {
          "quote": {
            "$ref": "#/components/schemas/EstimateQuoteV3"
          },
          "ignored": {
            "$ref": "#/components/schemas/IgnoredParametersV3"
          }
        }
      },
      "CreateTransactionRequestV3": {
        "type": "object",
        "description": "Body of POST /v3/transactions. An unknown field is refused with 400 rather than dropped.",
        "required": [
          "fromCurrency",
          "fromNetwork",
          "toCurrency",
          "toNetwork",
          "address"
        ],
        "properties": {
          "fromCurrency": {
            "type": "string",
            "maxLength": 32,
            "description": "Canonical ticker of the asset being sold, from GET /v1/assets."
          },
          "fromNetwork": {
            "type": "string",
            "maxLength": 32,
            "description": "Network of the asset being sold."
          },
          "toCurrency": {
            "type": "string",
            "maxLength": 32,
            "description": "Canonical ticker of the asset being bought."
          },
          "toNetwork": {
            "type": "string",
            "maxLength": 32,
            "description": "Network of the asset being bought."
          },
          "address": {
            "type": "string",
            "description": "Recipient address. It must belong to toCurrency on toNetwork: a payout sent on the wrong network is lost. It is checked against a format pattern and not against a checksum, so one mistyped character can pass."
          },
          "fromAmount": {
            "$ref": "#/components/schemas/DecimalAmountV3",
            "description": "Amount to send. Required unless type=reverse. Mutually exclusive with toAmount."
          },
          "toAmount": {
            "$ref": "#/components/schemas/DecimalAmountV3",
            "description": "Amount to receive. Required for type=reverse, which also needs flow=fixed-rate and a rateId. Mutually exclusive with fromAmount."
          },
          "extraId": {
            "type": "string",
            "description": "Memo or destination tag of the recipient address. Only for assets that use one; on any other asset it is refused."
          },
          "refundAddress": {
            "type": "string",
            "description": "Where the funds go back if the order cannot be completed. A refund travels the network the deposit came from, so it must belong to fromCurrency on fromNetwork."
          },
          "refundExtraId": {
            "type": "string",
            "description": "Memo or destination tag of the refund address."
          },
          "contactEmail": {
            "type": "string",
            "format": "email",
            "description": "Contact address stored with the order, when the person gave one. Ask for it rather than inventing one."
          },
          "flow": {
            "$ref": "#/components/schemas/EstimateFlow"
          },
          "type": {
            "$ref": "#/components/schemas/EstimateDirection"
          },
          "rateId": {
            "type": "string",
            "description": "The rateId of a quote. Required for flow=fixed-rate and for type=reverse, because only a locked quote can hold the amount that was asked for. It holds for about two minutes."
          },
          "promoCode": {
            "type": "string",
            "description": "The code the person holds. Quote with the same code the order will carry, or the amounts will not match."
          },
          "source": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9-_.]{1,100}$",
            "description": "Attribution of the surface the order came from, when there is one to declare."
          },
          "linkId": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9-_.]{1,100}$",
            "description": "Partner link identifier, when there is one."
          },
          "help": {
            "type": "boolean",
            "description": "Send {\"help\": true} on its own to get the manual of the version 3 endpoints as text/plain with status 200. No order is created."
          }
        }
      },
      "CreateTransactionResponseV3": {
        "type": "object",
        "description": "The created order. It commits nothing until the deposit reaches payinAddress.",
        "required": [
          "id",
          "provider",
          "fromCurrency",
          "fromNetwork",
          "toCurrency",
          "toNetwork",
          "fromAmount",
          "toAmount",
          "flow",
          "type",
          "payinAddress",
          "payinExtraId",
          "payoutAddress",
          "payoutExtraId",
          "refundAddress",
          "refundExtraId",
          "addressFormatChecked"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/OrderIdV3",
            "description": "Identifier of the order, to be passed to GET /v3/transactions/{id}."
          },
          "provider": {
            "$ref": "#/components/schemas/SwapProviderV3"
          },
          "fromCurrency": {
            "type": "string"
          },
          "fromNetwork": {
            "type": "string"
          },
          "toCurrency": {
            "type": "string"
          },
          "toNetwork": {
            "type": "string"
          },
          "fromAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount the person has to send."
          },
          "toAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount the person is expected to receive. On a floating rate it is an estimate and the final figure is set when the deposit arrives."
          },
          "flow": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          },
          "validUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "How long the person has to send the deposit at the locked rate, set by whoever executes the order. Present when the rate is locked. Read the field; the number is not fixed and is not the two minutes of a rateId."
          },
          "payinAddress": {
            "type": [
              "string",
              "null"
            ],
            "description": "The deposit address. This is the only address the person may be told to send to, exactly as it is written here."
          },
          "payinExtraId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Memo or destination tag the deposit must carry. When it is filled, a deposit sent without it can be lost."
          },
          "payoutAddress": {
            "type": [
              "string",
              "null"
            ],
            "description": "The recipient address as it was stored. Worth showing back to the person so they can confirm it."
          },
          "payoutExtraId": {
            "type": [
              "string",
              "null"
            ]
          },
          "refundAddress": {
            "type": [
              "string",
              "null"
            ],
            "description": "The refund address as it was stored. null when none was given, which means there is nowhere to send the funds back to."
          },
          "refundExtraId": {
            "type": [
              "string",
              "null"
            ]
          },
          "addressFormatChecked": {
            "type": "boolean",
            "description": "false when the service holds no address pattern for that asset and the address was accepted unchecked. true means the address matched a format pattern, which is not a checksum."
          },
          "addressWarning": {
            "type": "string",
            "description": "Present when addressFormatChecked is false: says in words that the address was not verified and has to be confirmed with the person before they send funds."
          },
          "ignored": {
            "$ref": "#/components/schemas/IgnoredParametersV3"
          }
        }
      },
      "TransactionStatusV3": {
        "type": "object",
        "description": "The state of one order. Every amount is a decimal string.",
        "required": [
          "id",
          "status",
          "provider"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/OrderIdV3"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "State of the order as the executor reports it. The values seen in practice are new, waiting, confirming, exchanging, sending, finished, failed, refunded, verifying, expired, refunding and rejected; treat the field as an open string and do not fail on an unlisted value."
          },
          "provider": {
            "$ref": "#/components/schemas/SwapProviderV3"
          },
          "fromCurrency": {
            "type": [
              "string",
              "null"
            ]
          },
          "fromNetwork": {
            "type": [
              "string",
              "null"
            ]
          },
          "toCurrency": {
            "type": [
              "string",
              "null"
            ]
          },
          "toNetwork": {
            "type": [
              "string",
              "null"
            ]
          },
          "fromLegacyTicker": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy ticker of the asset being sold, such as usdttrc20. It belongs in the web link https://changenow.io/exchange?from=<ticker>, never in a version 3 call."
          },
          "toLegacyTicker": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy ticker of the asset being bought."
          },
          "expectedAmountFrom": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount the order was created to receive."
          },
          "expectedAmountTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount the order was created to pay out."
          },
          "amountFrom": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount that actually arrived. null until the deposit is seen, so this is the field that answers whether the person has paid."
          },
          "amountTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount of the payout. It is filled before the payout happens and can carry fewer digits than expectedAmountTo, so it is not evidence that funds were sent: payoutHash is. Quote expectedAmountTo as the figure the order was created for."
          },
          "payinAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "payinExtraId": {
            "type": [
              "string",
              "null"
            ]
          },
          "payoutAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "payoutExtraId": {
            "type": [
              "string",
              "null"
            ]
          },
          "refundAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "refundExtraId": {
            "type": [
              "string",
              "null"
            ]
          },
          "payinHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Transaction hash of the deposit, once it is on chain."
          },
          "payoutHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Transaction hash of the payout."
          },
          "refundHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Transaction hash of the refund."
          },
          "refundAmount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amount that was refunded."
          },
          "validUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Deposit window of a locked rate. null on a floating rate."
          },
          "depositReceivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the deposit was seen."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ApiErrorCodeV3": {
        "type": "string",
        "enum": [
          "MISSING_PARAMETER",
          "INVALID_PARAMETER",
          "UNKNOWN_PARAMETER",
          "UNKNOWN_ASSET",
          "UNKNOWN_NETWORK",
          "AMBIGUOUS_ASSET",
          "AMOUNT_OUT_OF_RANGE",
          "INVALID_ADDRESS",
          "PAIR_NOT_AVAILABLE",
          "RATE_UNAVAILABLE",
          "NOT_FOUND",
          "AUTHENTICATION_REQUIRED",
          "NOT_ALLOWED",
          "METHOD_NOT_ALLOWED",
          "UNSUPPORTED_MEDIA_TYPE",
          "RATE_LIMITED",
          "UPSTREAM_UNAVAILABLE",
          "INTERNAL_ERROR"
        ],
        "description": "What kind of thing went wrong. The codes that arrive with 4xx name something in the request; RATE_LIMITED, UPSTREAM_UNAVAILABLE and INTERNAL_ERROR mean the request was fine and a retry is the right move."
      },
      "ApiFieldErrorV3": {
        "type": "object",
        "description": "One reason a request was refused, attached to the parameter that has to change.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ApiErrorCodeV3"
          },
          "message": {
            "type": "string",
            "description": "What is wrong and what to do about it, in English, written to be relayed or acted on directly."
          },
          "expected": {
            "type": "string",
            "description": "What a valid value for this parameter looks like."
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The complete set of accepted values, present only when the set is finite. When it is here, pick from it rather than guessing."
          },
          "min": {
            "type": "string",
            "description": "Smallest accepted amount, counted in unit. Present on AMOUNT_OUT_OF_RANGE below the minimum."
          },
          "max": {
            "type": "string",
            "description": "Largest accepted amount, counted in unit. Present on AMOUNT_OUT_OF_RANGE above the maximum."
          },
          "unit": {
            "type": "string",
            "description": "Currency min and max are counted in: fromCurrency for type=direct, toCurrency for type=reverse."
          }
        }
      },
      "ApiUpstreamResponseV3": {
        "type": "object",
        "description": "The refusal of the exchange service behind this API, quoted as it came, for logs and support. Present when an upstream refusal was mapped to one of these errors. The message here is not the instruction to follow: errors[field].message is.",
        "required": [
          "message"
        ],
        "properties": {
          "code": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ],
            "description": "The marker the exchange service used, when it gave one."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ApiErrorV3": {
        "type": "object",
        "description": "The refusal format of the version 3 endpoints. errors is keyed by the parameter that has to change, so read its keys before its messages. The key request means the refusal is about the request as a whole and not about one parameter.",
        "required": [
          "status",
          "errors",
          "help"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "description": "The HTTP status, repeated in the body. 4xx means change the request, 5xx means keep it and retry."
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/ApiFieldErrorV3"
            },
            "description": "One entry per parameter at fault, keyed by the parameter name."
          },
          "help": {
            "type": "string",
            "description": "Where the manual of these endpoints lives, as a call to make."
          },
          "upstreamResponse": {
            "$ref": "#/components/schemas/ApiUpstreamResponseV3"
          }
        }
      }
    }
  }
}
