CrestRate
Log inSign up
On this page

CrestRate API Reference

Multi-source aggregated exchange rates for businesses that charge or pay across currencies. You choose which side the rate favors, markup is configured per API key, and quotes lock both the rate and the markup.

  • Base URL — https://api.crestrate.com. Every rate endpoint lives under /v1, for example https://api.crestrate.com/v1/rate
  • First integration — start with the Quickstart; four copy-pasteable snippets get your first call working
  • Machine-readable spec — OpenAPI 3.1
  • Plans, quotas and rate limits — Pricing
  • Service status — https://crestrate.com/status

Use cases

If you price in one currency and settle in another, this API fits. We don't segment customers by industry; common uses:

  • Cross-border e-commerce — convert list prices into the customer's currency
  • SaaS and subscriptions — bill each customer in their local currency
  • Visitor spending in Japan — hotels, retail, duty-free shops and POS systems price in yen; visitors pay in Hong Kong dollars, yuan, US dollars or euros
  • B2B import and export — invoice in the buyer's currency, and lock the rate with a quote for as long as your offer is valid
  • Platforms and marketplaces — charge buyers with strategy=max, pay sellers out with strategy=min
  • Travel, study-abroad and visa agencies — quote in the client's currency
  • Games and digital goods — set regional prices

Supported currencies

Real-time data is available for the following currencies, in any base and quote combination:

Currency Code Smallest unit for amounts
US dollar USD 0.01
Japanese yen JPY 1
Euro EUR 0.01
Hong Kong dollar HKD 0.01
Chinese yuan CNY 0.01

Real-time data temporarily unavailable: TWD, SGD, GBP, AUD, KRW, MYR, THB. They will open once their data is stable.

Until then, a request that uses one of those codes returns HTTP 422 currency_not_available, with the list of available currencies in supported. A code we don't recognize at all returns HTTP 400 unsupported_currency. Currency codes are case-insensitive in requests and always uppercase in responses.

How the rate is chosen

Every request prices against every currently healthy source at once, not a single feed:

  1. Each source's rate table is normalized to a common base, and your pair is derived as a cross rate
  2. Sources with stale data, and sources that don't cover the pair, are dropped first
  3. The anchor is the median of the freshest available tier of sources (realtime → hourly → daily). That is the most reliable estimate of the current market price
  4. Sources deviating from the anchor beyond the fault threshold are treated as corrupt data and dropped
  5. A surviving rate is selected per strategy: max takes the highest, min the lowest, median the one closest to the median. It is then clamped both ways: above the cap cap_rate it is pulled down, below the floor floor_rate it is lifted up
  6. Markup is applied last, in the direction of your side: up when charging, down when paying

The response always states which source the final rate came from, what every other source quoted, and any source that was dropped along with a machine-readable code (excluded_code). Sources are identified by stable anonymous IDs such as source_a. If too few valid sources remain, you get HTTP 503 no_rate instead of a number we can't stand behind.

You choose which side the rate favors

On the same pair, a higher rate is better when you charge and a lower one is better when you pay. Tell us your side with strategy; the response field side states which side was applied.

strategy side Favors Typical use
max (default) charge The party charging: the highest rate, so the same list price brings in more E-commerce, SaaS, hotels, B2B invoicing
min pay The party paying: the lowest rate, so the same amount owed costs less Marketplace seller payouts, paying overseas suppliers
median charge Neutral: the rate closest to the median of all sources Bookkeeping, internal reporting

strategy is case-insensitive; any other value returns HTTP 400 bad_request. side also sets the direction of markup and of rounding:

Item charge (charging) pay (paying)
Markup The rate moves up; the customer pays more The rate moves down; you pay out less
payable rounding Up to the currency's smallest unit Down to the currency's smallest unit
gain_vs_mid payable - mid_payable (extra collected) mid_payable - payable (saved on payout)

Both directions are guarded. The selected rate is compared with the freshness anchor anchor_rate: above the cap cap_rate it is pulled back (capped: true); below the floor floor_rate it is lifted back (floored: true). When charging it can't sit meaningfully above the live market, and when paying it can't sit meaningfully below it — so it holds up when the other party checks a public converter. Markup is your own explicit setting and is applied after this clamp.

Markup

Markup is the margin you add on top of the selected rate, in basis points (bps): 100 bps = 1%.

Item Details
Where to set it The API keys list in the dashboard — each key has its own markup
Default markup The markup this key uses for every currency
Per-currency overrides Override the default for a given quote currency, e.g. HKD at 120 bps and JPY at 200 bps. Only currencies with real-time data can be overridden
Range 0 to 1,000 bps (0% to 10%). 0 means explicitly no markup
When it applies Immediately after saving, from the next request
Quotes Markup is locked in when a quote is created. Changing it later does not affect quotes already issued

Precedence, highest first. The response field markup_source says which one applied:

  1. The key's override for this currency → key_currency
  2. The key's default markup → key_default
  3. The platform default → platform

Same-currency pairs (e.g. USD → USD) involve no conversion, so no markup is applied and markup_source is none.

Direction and rounding follow side, so the result always favors you. Charging: raw_rate × (1 + markup_bps/10000), amounts round up. Paying: raw_rate × (1 - markup_bps/10000), amounts round down.

Worked example (illustrative numbers, not live rates; for an easy comparison both cases assume a selected rate of 7.8000): a US$1,000 order settled in Hong Kong dollars, with a markup of 150 bps (1.5%).

Scenario No markup Markup 150 bps Difference
You're charging (strategy=max) Rate 7.8000, customer pays HK$7,800.00 Rate 7.9170, customer pays HK$7,917.00 HK$117.00 more collected
You're paying (strategy=min) Rate 7.8000, you pay HK$7,800.00 Rate 7.6830, you pay HK$7,683.00 HK$117.00 less paid out

Preview before saving. When you set a markup, the dashboard shows the effect as you adjust the value: how much more the customer pays, or how much less you pay out.

Disclosure. Where markups must be disclosed (for example, consumer payments in the EU), disclose yours to your customers.

Locked quotes

A quote freezes the price you show your customer until it expires, so rate movement between display and payment can't produce mismatched totals.

  • POST /v1/quotes locks the final rate, the markup_bps in effect, and the settlement amount payable at creation time
  • Changing the key's markup in the dashboard afterwards does not change a quote already issued
  • The quote holds for ttl_seconds (currently 900 seconds, i.e. 15 minutes). Read the value from the response instead of hard-coding it
  • Keep quote_id and read the quote back with GET /v1/quotes/{quote_id} when the customer pays; once expires_at has passed the lookup returns 404, and you should create a new quote

Website demo

The live demo on the home page is for evaluation only, and it is not an API to integrate against:

  • Its data is delayed by 1 hour: it prices with the latest snapshot each source had one hour ago, and shows the data time next to the result
  • Every request passes an invisible human-verification check (Cloudflare Turnstile)
  • It uses the platform default markup, is rate-limited per network and globally, and may return "busy" at peak times
  • It does not accept API keys and is not metered against any account

For live data, create a free API key in the dashboard.

API reference

Authentication

Every /v1 endpoint requires an API key. There is no anonymous access.

Header Example
X-API-Key X-API-Key: <your key>
Authorization Authorization: Bearer <your key>
  • Send either one; if both are present, X-API-Key is used
  • Create and revoke keys in the dashboard. You need a verified email address first, and an account can hold up to 10 active keys. The plaintext key is shown only once
  • A revoked key stops working immediately (HTTP 401 invalid_api_key)
  • Call the API from your server. /v1 responses carry no CORS headers, and a key embedded in a web page or mobile app can be copied by anyone
  • GET /health and GET /openapi.json need no key

Common conventions

Convention Details
Rates and amounts are always strings Not JSON numbers. A JSON number is an IEEE 754 double; a value like 0.05117904 loses precision when parsed that way and your amounts come out wrong. Parse with Decimal (Python) / BigDecimal (Java) / decimal.js (JS). Rates have 8 decimal places
Timestamps Always ISO 8601 UTC strings, e.g. 2026-09-14T10:38:01Z. Where arithmetic is likely, a *_unix companion field (Unix seconds, float) is also provided
Percentages Strings, expressed in % — not as fractions. "0.1791" means 0.1791%, not 17.91%
Currency codes ISO 4217. Case-insensitive in requests, uppercase in responses. See Supported currencies
base / quote base = the currency the price is set in, quote = the settlement currency (what the customer pays in when you charge, what you pay out in when you pay). A rate always means "how much quote one unit of base buys"
Caching headers Rate responses are sent with Cache-Control: no-store. The API never sets cookies

Rate limits and monthly quota

Each plan has a rate limit (a steady rate plus a short burst) and a monthly request quota, published on the pricing page.

  • Both are counted per account, not per key. All API keys on the same account share one rate limit and one monthly quota; creating more keys does not raise either. The X-Quota-* headers show the account-wide total
  • The monthly quota resets at the start of each calendar month (UTC)
  • Every request that passes authentication and the limits counts toward the quota, whatever its result — including 400, 404, 422 and 503 responses. Requests rejected with 401, 403 or 429 do not count
  • HEAD requests to /v1 endpoints are processed exactly like GET — the full rate calculation runs — so they count toward the quota and the rate limit in the same way
  • Paths that don't exist return 404 not_found and are not metered

Every /v1 response — including errors — carries these headers:

Header Meaning
X-RateLimit-Limit Maximum burst: how many requests you can send back-to-back
X-RateLimit-Remaining How many requests you can send right now without waiting
X-RateLimit-Reset Unix time (seconds) when the next request slot becomes available
X-Quota-Limit Monthly quota of the account. 0 means unlimited
X-Quota-Used Requests the account has used this month, including this one
X-Quota-Remaining Requests left this month. Always 0 when X-Quota-Limit is 0 (unlimited), so check the limit first
X-Request-Id ID of this request. Include it when you contact support
Retry-After Seconds to wait. Sent only with 429 rate_limited

When a request can't be attributed to an account (a missing or invalid key, a disabled or unverified account, an oversized body, or an internal error), the rate and quota headers carry neutral default values that don't reflect any account.

Hitting a limit

Response What it means What to do
429 rate_limited Too many requests in a short time Wait for Retry-After seconds (the body also carries retry_after), then retry
429 quota_exceeded The account's monthly quota is used up. A hard block: no overage billing, no Retry-After Don't retry. Upgrade (upgrade_url is the absolute address of the pricing page) or wait for next month. The body carries used and quota

Request processing

Requests are checked in this order; the first failing check determines the response:

  1. Body size: a request body larger than 16 KiB → 413 payload_too_large
  2. API key missing → 401 api_key_required; invalid or revoked → 401 invalid_api_key
  3. Account disabled → 403 account_disabled; email address not verified → 403 email_not_verified
  4. Rate limit → 429 rate_limited; monthly quota → 429 quota_exceeded
  5. Parameters → 400 bad_request / 400 unsupported_currency / 422 currency_not_available (already counted toward the quota)
  6. Pricing → 503 no_rate when too few valid sources; 503 storage_unavailable when our data store is briefly unreachable

GET /v1/rate

Returns the final rate for a single pair, along with the full per-source breakdown.

Query parameters

Parameter Required Details
base Yes Pricing currency. Alias from
quote Yes Settlement currency. Alias to
strategy No Which side the rate favors: max (default, charging) / min (paying) / median (neutral). Case-insensitive

A same-currency pair (e.g. base=USD&quote=USD) returns rate 1.00000000 with chosen_provider and anchor_tier set to identity.

Response: top-level fields

Field Type Description
base string Pricing currency
quote string Settlement currency
rate string The final rate — this is the one you calculate amounts with. raw_rate × (1 + markup_bps/10000) when charging (side=charge), raw_rate × (1 - markup_bps/10000) when paying (side=pay). Equals raw_rate when markup_bps is 0
raw_rate string The rate before markup. Selected across sources per strategy; already clamped to cap_rate / floor_rate if either was hit
mid_rate string Median of the valid sources, roughly the market mid. Use it to measure how far above market you are
low_rate string Lowest rate among valid sources. Equals raw_rate when strategy=min and the floor wasn't hit
high_rate string Highest rate among valid sources. Equals raw_rate when strategy=max and the cap wasn't hit
markup_bps int The markup actually applied to this call, in basis points (100 bps = 1%). Always 0 for same-currency pairs
markup_source string Where the markup came from: key_currency (this key's override for the currency) / key_default (this key's default markup) / platform (platform default) / none (same-currency pair, no markup)
strategy string The selection strategy used: max / median / min
side string Which side this call favored: charge (charging; max and median) / pay (paying; min). Markup direction and rounding follow it
chosen_provider string Which source the final rate came from, as an anonymous ID such as source_a. identity for same-currency pairs
source_count int Number of valid sources that took part in pricing (excludes dropped ones)
spread_vs_mid_pct string Deviation of the final rate from mid_rate, in %, 4 decimal places. Usually positive when charging, negative when paying
anchor_rate string The freshness anchor. Both outlier filtering and the cap and floor are measured against it; it's the median of the freshest available tier
anchor_tier string Which tier the anchor came from: realtime / hourly / daily / all / identity. Falling back to daily or all means the fresh sources are all unavailable and the quote may drift from market
anchor_notes string[] Why fresher tiers were not used as the anchor, in English. Empty array under normal conditions
cap_rate string | null The cap: anchor_rate plus a fixed band. raw_rate never exceeds it. Null for same-currency pairs
capped bool true means the selected rate exceeded the cap and was pulled back down. It signals the market is moving while slower sources lag — worth alerting on
floor_rate string | null The floor: anchor_rate minus the same band. raw_rate never falls below it. Null for same-currency pairs
floored bool true means the selected rate was below the floor and was lifted back up. Most common with strategy=min
sources object[] Full per-source breakdown, including dropped sources. See below
generated_at string When this response was generated (ISO 8601 UTC)
generated_at_unix float Same, as Unix seconds

Response: each sources[] entry

Field Type Description
provider string Stable anonymous source ID, e.g. source_a
label string Display name, e.g. Source A
data_freq string | null How often the source's own data updates: realtime / hourly / daily
rate string | null The base→quote rate this source reported. null means the source doesn't carry this pair
vs_anchor_pct string | null How far this source deviates from anchor_rate, in % (positive = higher)
fetched_at string When we fetched it (ISO 8601 UTC)
age_seconds float Seconds since that fetch
source_time string | null When the source itself claims the data is from. null = the source doesn't publish this
used bool Whether it took part in this pricing
chosen bool Whether this is the source that was selected
excluded_code string | null Machine-readable reason it was dropped; null if it was used. Match on this in monitoring
excluded_reason string | null Human-readable English reason; null if it was used. The wording may change — don't parse it

Values of excluded_code

excluded_code Example excluded_reason Meaning What to do
missing_currency Source does not cover this currency pair The source doesn't cover this pair Normal — other sources cover for it
snapshot_stale Snapshot is stale (9000s > 7200s) We haven't fetched fresh data from that source for too long Other sources cover for it; if it persists, check the status page
deviates_from_anchor Deviates 12.5% from the realtime tier anchor, beyond +/-5%; treated as a data fault The source's value is too far from the anchor That source's data is likely corrupt; nothing to do on your side

GET /v1/rates

A rate table for many pairs in one call. Rows are a trimmed-down version of GET /v1/rate and omit the sources breakdown.

Query parameters

Parameter Required Details
base No Comma-separated pricing currencies, e.g. USD,EUR. Empty items and duplicates are ignored
quote No Comma-separated settlement currencies, e.g. JPY,HKD

When a parameter is omitted, the service's default currency grid is used; pass both explicitly if your integration depends on the exact rows. If a list contains an unknown code the whole request returns 400 unsupported_currency; if it contains a currency without real-time data, 422 currency_not_available. The whole base list is checked before quote: within one list an unknown code takes precedence over a currency without real-time data, but a problem in base is always reported first, so a currency without real-time data in base combined with quote=ZZZ returns 422.

Rows use the platform default markup, not your key's markup. Use GET /v1/rate or GET /v1/convert when you need your own markup applied.

Response

Field Type Description
rows object[] One row per base × quote pair
generated_at / generated_at_unix string / float Generation time

Each rows[] entry

Field Type Description
base / quote string The currency pair
final_rate string Final rate with the platform default markup
raw_rate string Before markup
mid_rate string Median across sources
chosen_provider string The selected source (anonymous ID)
source_count int Number of valid sources
markup_bps int Markup applied (platform default)
spread_pct string Premium over the mid, in %, 3 decimal places
anchor_tier string Anchor tier
capped bool Whether the cap was hit
error string | null When not null, that row has no rate: only base, quote and error are present. Typical cause: too few valid sources for that pair. Always null-check before using a row

GET /v1/convert

Converts an amount in the pricing currency into the settlement amount. Amounts round to the currency's smallest unit (JPY to 1; USD, EUR, HKD and CNY to 0.01), and the rounding direction follows side: up when charging, down when paying, so the fraction always favors you.

Query parameters

Parameter Required Details
base / quote Yes Same as GET /v1/rate, including the aliases from / to
amount Yes Amount in the pricing currency as a decimal string, must be > 0. The converted amounts (payable, mid_payable), rounded to the settlement currency's smallest unit, may have at most 28 digits including decimal places; a larger amount returns 400 bad_request
strategy No Same as GET /v1/rate

Response

Field Type Description
base / quote string The currency pair
amount string The amount you passed in, as a decimal string
payable string The settlement amount. When charging (side=charge) it is what the customer owes, rounded up; when paying (side=pay) it is what you pay out, rounded down
rate string The final rate used
mid_rate string Market mid
mid_payable string The amount at the mid, rounded half-up, for comparison
gain_vs_mid string The difference in your favor — larger is better. Charging: payable - mid_payable (extra collected). Paying: mid_payable - payable (saved on payout)
markup_bps int Markup applied
markup_source string Where the markup came from, same as GET /v1/rate
side string charge / pay, same as GET /v1/rate
chosen_provider string Which source the rate came from (anonymous ID)
source_count int Number of valid sources
anchor_tier string Anchor tier
capped bool Whether the cap was hit
floored bool Whether the floor was hit

GET /v1/convert only converts — it does not lock the price. Use POST /v1/quotes for real orders.


POST /v1/quotes

Creates a locked quote: the rate, the markup and payable hold for ttl_seconds, and settlement uses payable.

Request body (JSON object, Content-Type: application/json, at most 16 KiB)

Field Required Details
base / quote Yes The currency pair. Aliases from / to
amount Yes Amount in the pricing currency, must be > 0. A decimal string is recommended; a JSON number is also accepted
reference No Your own order identifier (at most 128 characters), stored and echoed back for reconciliation
strategy No Which side the rate favors, same as GET /v1/rate

A body that isn't valid JSON, isn't a JSON object, or has a field of the wrong type returns 400 bad_request.

Response (HTTP 201)

Field Type Description
quote_id string Quote ID (32 lowercase hex characters). Store it for later lookups and reconciliation
reference string | null Echoes the order identifier you passed
base / quote string The currency pair
amount string Amount in the pricing currency
payable string The settlement amount — the locked value. It will not change before expiry. Rounding follows side
rate string The locked final rate
raw_rate string The rate before markup
mid_rate string Market mid at the time of the order, for reconciliation
markup_bps int Markup locked in at creation. Changing the markup in the dashboard later does not affect this quote
strategy string Selection strategy
side string charge / pay, same as GET /v1/rate
chosen_provider string Which source the rate came from (anonymous ID) — your evidence in a dispute or reconciliation
source_count int Number of valid sources at the time
created_at / created_at_unix string / float Creation time
expires_at / expires_at_unix string / float Expiry time. After it passes, lookups by that ID return 404
ttl_seconds int Lock duration in seconds

If the quote cannot be stored, the response is 503 storage_unavailable and no quote exists — retry the request.

GET /v1/quotes/{quote_id}

Reads a quote back. The fields are identical to those returned at creation, plus one:

Field Type Description
expired bool Whether it has expired. Quotes are removed as soon as they expire, so you'll normally get a 404 instead; this field can only be true in the narrow window before removal
  • quote_id that isn't 32 lowercase hex characters, or a quote that doesn't exist or has expired → 404 not_found
  • 503 storage_unavailable means we couldn't check right now — it does not mean the quote expired. Retry

GET /v1/sources

Which sources currently feed your rates, and how fresh each one is. Use it to judge how much to trust a given rate.

Response

Field Type Description
sources object[] The sources currently enabled
source_count int Number of sources
last_poll_at string | null When we last fetched (ISO 8601 UTC)

Each sources[] entry

Field Type Description
provider string Anonymous source ID; matches sources[].provider in GET /v1/rate
label string Display name of the source
data_freq string How often the source's own data updates: realtime / hourly / daily
last_fetch_at string | null When we last fetched from that source
last_fetch_age float | null Seconds since the last fetch
source_time string | null When the source itself claims the data is from. null = the source doesn't publish this
source_age_seconds float | null Age of the source's data in seconds
currencies int How many of the supported currencies the source currently covers

These two timestamps mean very different things. last_fetch_at is when we pulled the data, so it's always recent; source_time is when the source itself says the data is from. Daily sources typically lag 12–36 hours, and some daily sources can exceed 80 hours over a weekend — that's normal. Judging freshness from last_fetch_at alone will mislead you badly.


GET /health

Service health check, no API key required. For your monitoring or load balancer.

Field Type Meaning
status string ok / degraded. Reflects only "can we quote a rate right now"
sources_healthy int How many sources currently have fresh, usable data
last_updated_at string | null When the most recent source data was fetched (ISO 8601 UTC)

HTTP status: 200 with ok, 503 with degraded.

This endpoint deliberately returns a single health signal and nothing about our internal polling state or upstream error detail. For per-source freshness use GET /v1/sources, or visit the status page.

GET /openapi.json

The OpenAPI 3.1 document for every endpoint above, at https://api.crestrate.com/openapi.json. No API key required; it can be fetched from a browser (CORS allowed) and is cached for 5 minutes. It declares both authentication methods (X-API-Key and Bearer), so SDK generators can use it as is.

https://crestrate.com/openapi.json on the website answers with an HTTP 302 redirect to this address. Point tools at the API address directly.

Errors

Errors have the same shape across every endpoint:

Field Type Description
error string Machine-readable error code. Match on this, not on message
message string Human-readable English description. The wording may change
currency string The offending currency code (unsupported_currency, currency_not_available)
supported string[] Currencies with real-time data (currency_not_available)
retry_after number Seconds to wait (rate_limited)
used / quota int Requests used this month / monthly quota (quota_exceeded)
upgrade_url string Absolute URL of the pricing page (quota_exceeded)
HTTP error Meaning Retry?
400 bad_request Missing or invalid parameters: no base/quote, amount missing, ≤ 0, not a number or too large, unknown strategy, invalid JSON body, reference too long No — fix the request
400 unsupported_currency Unknown currency code. Carries currency No
401 api_key_required No API key was sent No — send X-API-Key or Authorization: Bearer
401 invalid_api_key The key is invalid or has been revoked No
403 account_disabled The account has been disabled. Contact support No
403 email_not_verified The account's email address is not verified yet After verifying
404 not_found Quote not found or expired; unknown path No
413 payload_too_large Request body larger than 16 KiB No
422 currency_not_available Real-time data for this currency is temporarily unavailable. Carries currency and supported Not until the currency opens
429 rate_limited Rate limit exceeded. Retry-After header and retry_after field Yes, after Retry-After
429 quota_exceeded Monthly quota used up. Carries used, quota, upgrade_url No — upgrade or wait for next month
500 internal_error Unexpected server-side error Yes, with backoff; quote X-Request-Id to support if it persists
503 no_rate Not enough valid sources to produce a rate we trust. message lists why each source was excluded (anonymous IDs only) Yes, later; fall back to your own rate meanwhile
503 storage_unavailable Service temporarily unavailable; nothing was stored Yes, with backoff

Example:

{
  "error": "currency_not_available",
  "message": "Real-time data for GBP is temporarily unavailable. Supported: USD, JPY, EUR, HKD, CNY.",
  "currency": "GBP",
  "supported": ["USD", "JPY", "EUR", "HKD", "CNY"]
}

503 no_rate is deliberate — we would rather return no price than one whose provenance is questionable. Callers should retry later or fall back to their own backstop rate. Do not treat it as 0.


Best practices

Pick the right side with strategy. Use the default max when collecting from customers, min when paying sellers or suppliers, and median for bookkeeping only. Pick the wrong one and rate selection, markup and rounding all work for the other party.

Parse rates with a decimal type. Never use floating point. Every rate and amount is a JSON string for exactly this reason. Parsed as an IEEE-754 double, 0.05117904 loses precision and your totals drift over time. Use Decimal / BigDecimal / decimal.js / bcmath.

Always lock the price before collecting. Time passes between the moment the customer sees a price and the moment they pay, and the rate moves in that gap. POST /v1/quotes freezes the rate, the markup and the amount until the TTL runs out and hands you a quote_id for later reconciliation. GET /v1/convert is for display and estimates only — don't place orders with it.

rate_limited is worth retrying; quota_exceeded isn't. The first is transient — back off per Retry-After and try again. The second is a hard block for the rest of the calendar month, shared by every key on your account. Upgrade, or wait for the reset next month.

Never treat 503 no_rate as 0. It means "too few valid sources — we won't give you a price we can stand behind." Fall back to your own backstop rate, or pause the ordering flow. Do not multiply by 0.

Validate currencies on your side. A request with a currency that has no real-time data still counts toward your quota; check against the supported list before calling.

Cache on your side. Rates can't change faster than our fastest source. Caching for 30–60 seconds cuts your usage dramatically at almost no cost in accuracy.

Watch capped, floored and anchor_tier. capped: true or floored: true means the market is moving while the slower sources haven't caught up, and we clamped the quote back toward the live market. An anchor_tier that drops to daily or all means our freshest sources are temporarily unavailable — rates are still usable, but less fresh than usual.

Monitor on error and excluded_code, not on text. Messages and excluded_reason are for humans and may be reworded.

Versioning policy

This is v1, and the version is part of the path (/v1). We add fields without notice, so parse defensively and ignore keys you don't recognize.

We will not remove or repurpose an existing field, change a field's type, or change the meaning or HTTP status of an error code with less than 90 days' notice. Notice goes to the account email of every customer with recent traffic.

Support

Questions, or found behavior that doesn't match these docs? Email [email protected] and include the X-Request-Id of the request.