API reference · v1
The currency exchange API, on one page.
Two GET endpoints, one response shape. Everything below is what the API answers today — fields, headers, limits and error codes included. There is no SDK to install and no webhook to configure.
- Base URL
- fiatagent.base44.app
- Format
- JSON · UTF-8
- Auth
- Bearer key
- CORS
- Any origin
Overview
Readings are held in Fiat Agent's own store and served from it directly, so a call is a single fast read — never a wait on a third-party provider.
Endpoints
| Field | Type | Notes |
|---|---|---|
| GET /api/v1/public/rates | No key | Latest rates and the currency catalogue · Limited per client |
| GET /api/v1/rates | Bearer key | Latest rates, catalogue, history and balance · No rate limit |
Responses are plain JSON with permissive CORS headers, so a browser, a serverless function or a notebook can all read them. Rates are quoted against one stored base; asking for another base re-quotes every value by division, and a cross rate between any two currencies is the quotient of their two quotes.
Quickstart
Three steps, about a minute.
- Create account
01
Create an account
Free and card-free. Your 5,000-request signup bonus lands the moment the account exists — it never resets and never expires.
- Open API screen
02
Mint a key
In the API screen of your rate desk. The secret is shown once, so copy it before you close the panel.
03
Send the key
As a bearer token on a GET request. There is no per-minute cap on a keyed call, and every response reports the balance left.
curl · keyed endpoint
curl -s "https://fiatagent.base44.app/api/v1/rates?base=EUR&snapshots=48" \
-H "Authorization: Bearer fiatagent_…"Authentication
The open endpoint needs no credentials. The keyed endpoint takes a bearer token minted in your account.
Request header
Authorization: Bearer fiatagent_…- Keys come from a cryptographically secure random source and are shown exactly once.
- Only a one-way SHA-256 digest is stored, so a copy of our data cannot be replayed against the API.
- Revoking a key takes effect on its next request — there is nothing to flush or expire.
- A key belongs to an account, not a browser: keep it on a server you control.
Endpoints
Both answer GET. Both accept ?base. Only the keyed endpoint returns history and reports a balance.
GET/api/v1/public/rates
No keyOpen to anyone, no credential. Returns the current reading and every currency with its name, symbol, flag and quote. No stored history, so the body stays small.
fiatagent.base44.app/api/v1/public/rates
GET/api/v1/rates
Bearer keyThe full response: the current reading, the catalogue, up to 2,000 stored readings and the account's remaining balance. A dated range (?from and ?to) reaches every reading we hold, so one call gives you a month, a quarter or the whole record. Keyed calls carry no rate window at all — a burst is answered as it arrives, and each call is counted once against the balance.
fiatagent.base44.app/api/v1/rates
Query parameters
| Field | Type | Notes |
|---|---|---|
| ?base | string | A three-letter code to re-quote every rate against. Applies to both endpoints. A malformed value answers 400 invalid_parameter; a well-formed code we do not publish answers 400 invalid_base. |
| ?snapshots | integer | How many stored readings to return, 1–2,000. Defaults to 2,000. Keyed endpoint only. |
| ?from | date | First day of stored history to return, written YYYY-MM-DD in UTC and inclusive. Keyed endpoint only. |
| ?to | date | Last day of stored history to return, YYYY-MM-DD in UTC, inclusive of that whole day. Keyed endpoint only. Omit either bound to leave that side open. A ?to= earlier than ?from= answers 400 invalid_parameter rather than an empty list. |
Machine-readable spec
Both endpoints are published as an OpenAPI 3.1 document, so a client can be generated from it or imported straight into Postman, Insomnia or any OpenAPI-based tool. In Postman choose Import → Link and paste the address below.
Parameters
Four optional parameters. Anything we cannot honour is refused with a 400 that names it, rather than being silently ignored.
Accepted on GET
| Field | Type | Notes |
|---|---|---|
| ?base | string | A three-letter code to re-quote every rate against. Applies to both endpoints. A malformed value answers 400 invalid_parameter; a well-formed code we do not publish answers 400 invalid_base. |
| ?snapshots | integer | How many stored readings to return, 1–2,000. Defaults to 2,000. Keyed endpoint only. |
| ?from | date | First day of stored history to return, written YYYY-MM-DD in UTC and inclusive. Keyed endpoint only. |
| ?to | date | Last day of stored history to return, YYYY-MM-DD in UTC, inclusive of that whole day. Keyed endpoint only. Omit either bound to leave that side open. A ?to= earlier than ?from= answers 400 invalid_parameter rather than an empty list. |
- ?base=EUR re-quotes every rate. A malformed value answers 400, and so does a code we do not publish — neither one spends any of your balance.
- ?snapshots=50 trims the history array. Anything outside 1–2,000 answers 400 invalid_parameter.
- ?from=2026-10-01&to=2026-10-31 answers with the readings stored inside those two UTC days, both ends inclusive. Either bound can stand alone, and a date written any other way answers 400 invalid_parameter.
- Anything else — symbols, amount — answers 400 unsupported_parameter rather than being silently ignored.
Response fields
Every response is an envelope: ok, meta and data. Errors replace data with a single error object.
meta · open endpoint
| Field | Type | Notes |
|---|---|---|
| base_currency | string | Currency every rate in this response is quoted against. |
| currency_count | integer | How many currency codes the response carries. |
| updated_at | ISO 8601 | When we read these rates from the source. Never a promise about how fresh the source's own figures are. |
| freshness | object | fetched_at, source_updated_at (the provider's publication time), age_minutes and stale — whether the reading is behind our sync cadence. Read this to decide whether a figure is old enough to matter. |
| tier | string | "public" on the open endpoint. |
| limits | array | The three published windows, as window and limit pairs. |
meta · keyed endpoint
| Field | Type | Notes |
|---|---|---|
| base_currency | string | Currency every rate in this response is quoted against. |
| currency_count | integer | Currency codes in the response. |
| snapshot_count | integer | Readings included in data.snapshots. |
| snapshot_limit | integer | Most readings one response can carry. |
| updated_at | ISO 8601 | When we read these rates from the source. |
| freshness | object | fetched_at, source_updated_at, age_minutes and stale — the same block the open endpoint returns. |
| tier | string | "account" on a keyed call. |
| limits | array | This key's windows, as window and limit pairs. |
| quota | object | The account's request balance — see the quota fields below. |
data.latest
| Field | Type | Notes |
|---|---|---|
| base_currency | string | Base these rates are quoted against. |
| updated_at | ISO 8601 | When we read the reading these rates came from. |
| source_updated_at | ISO 8601 | When the underlying source last published rates. Equal to updated_at only when the source republishes on every read. |
| age_minutes | integer | How many minutes old the stored reading is, counted from updated_at. |
| currency_count | integer | Codes in rates. |
| rates | object | Map of currency code to quote against base_currency. |
data.currencies[]
| Field | Type | Notes |
|---|---|---|
| code | string | ISO 4217 code as published. |
| name | string | Official currency name. |
| symbol | string | Display symbol; the code itself when the currency has none. |
| flag | string | null | Flag image URL, 80px wide, or null for codes with no single territory. |
| rate | number | null | Quote against base_currency, or null when no quote is held. |
data.snapshots[] · keyed endpoint
| Field | Type | Notes |
|---|---|---|
| fetched_at | ISO 8601 | When the reading entered our store. Sorted newest first. |
| base_currency | string | Base this reading is reported against — always the requested base. |
| updated_at | ISO 8601 | Timestamp of the reading itself. |
| currency_count | integer | Codes in rates. |
| rates | object | Map of currency code to quote. |
meta.quota · keyed endpoint
| Field | Type | Notes |
|---|---|---|
| limit | integer | Total balance: your signup bonus plus every pack bought. |
| used | integer | Keyed requests spent all time, across every key on the account. |
| remaining | integer | Requests left in the balance; nothing resets, so it moves only with use and purchases. |
Reading history
Each reading is stored as its own record, so a series needs no second data source and no reconciliation.
- Up to 2,000 readings per call, newest first, trimmed with ?snapshots.
- Every reading we hold is reachable: ?from and ?to take UTC calendar days and return the readings inside them, both ends inclusive.
- Every reading is re-quoted against the base you ask for, so a series never mixes bases.
- Timestamps are ISO 8601 in UTC; ?snapshots=1 gives just the latest reading.
- History is served from our own store only, never fetched upstream on demand — which is why an answer arrives in milliseconds whatever range you ask for.
- The open endpoint carries no history at all; it always answers with the current reading.
JavaScript · building a series
// Every response carries the readings behind the latest quote, oldest last.
const series = data.snapshots
.map((reading) => ({
t: reading.fetched_at,
eurPerUsd: reading.rates.EUR / reading.rates.USD,
}))
.reverse();
// A cross rate is an exact division of two published quotes.
const quote = data.latest.rates.EUR / data.latest.rates.USD;curl · history between two dates
# from and to are UTC calendar days, both ends inclusive.
curl -s "https://fiatagent.base44.app/api/v1/rates?base=EUR&from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer fiatagent_…"Limits & balance
The open endpoint is rate-limited per client. A keyed call has burst windows per key, and its volume is bounded by the account's one-time request balance.
Public
No account
One open endpoint for exploration, prototypes and low-volume checks.
- Per second
- 5 / second
- Per hour
- 50 / hour
- Per day
- 50 / day
- Account balance
Free
With an API key
Create an account, mint a key, and call the full endpoint with any date range of history — bursts are capped, spending is bounded by your balance.
- Per second
- 20 / second
- Per hour
- 2,000 / hour
- Per day
- No daily cap
- Account balance
- 5,000 bonus
Signup bonus · shared by every key
Top-ups
BuyFrom $4 · one-time
Buy extra requests with PayPal when the balance runs down: 2,000 for $4 up to 100,000 for $87.50.
- Per second
- Unchanged
- Per hour
- Unchanged
- Per day
- Unchanged
- Account balance
- +100,000 to balance
One-time · never expires
Open endpoint · per client
| Field | Type | Notes |
|---|---|---|
| per second | 5 requests | Rolling UTC window. The call that would exceed it answers 429. |
| per hour | 50 requests | Rolling UTC window. The call that would exceed it answers 429. |
| per day | 50 requests | Rolling UTC window. The call that would exceed it answers 429. |
Keyed endpoint · per key
| Field | Type | Notes |
|---|---|---|
| per key, per second | 20 requests | Burst protection, set far above working load. A refused call draws nothing from the balance. |
| per key, per hour | 2000 requests | Burst protection, set far above working load. A refused call draws nothing from the balance. |
- Public windows are counted per client fingerprint on rolling UTC boundaries; a digest of the client address is stored, never the address itself.
- The 5,000-request signup bonus is yours from the moment the account exists. It never refreshes and never expires, and every key on the account draws from that one balance.
- A keyed call is capped per key on bursts, not on volume: 20 a second and 2,000 an hour per key, with no daily cap. The windows are set far above working load, and every keyed response reports its remaining room in the x-ratelimit-* headers.
- Both endpoints answer 429 when a window is spent, and a refused call spends nothing: a bad parameter names the parameter and costs no balance.
- Top-ups are one-time PayPal purchases that add to the same balance. They never expire, and they are shared by every key on the account.
Headers
Limits are readable from every response, so a client can pace itself without waiting for a 429.
Open endpoint
| Field | Type | Notes |
|---|---|---|
| x-ratelimit-limit | integer | Requests allowed in the tightest window for this client. |
| x-ratelimit-remaining | integer | Requests left in that window after this call. |
| x-ratelimit-reset | seconds | Until that window rolls over. |
| access-control-allow-origin | * | Every response is readable from a browser. |
| cache-control | no-store | Responses are never cached by a proxy. |
Keyed endpoint
| Field | Type | Notes |
|---|---|---|
| x-quota-limit | integer | The account's total balance. |
| x-quota-remaining | integer | Requests left in the balance after this call. |
| x-ratelimit-limit | integer | Requests allowed in the tightest window for this key. |
| x-ratelimit-remaining | integer | Requests left in that window after this call. |
| x-ratelimit-reset | seconds | Until that window rolls over. |
| retry-after | seconds | Sent with a 503: when to call again. |
| access-control-allow-origin | * | Every response is readable from a browser. |
| cache-control | no-store | Responses are never cached by a proxy. |
Errors
A failed call is still JSON: ok is false and error carries a machine-readable code, a message and, where it applies, a retry hint.
Error codes
| Field | Type | Notes |
|---|---|---|
| missing_key | 401 | A keyed call arrived without a bearer credential. |
| invalid_key | 401 | The key is not recognised. |
| revoked_key | 401 | The key was revoked by its owner and no longer works. |
| invalid_parameter | 400 | base, snapshots, from or to is malformed; error.parameter names it. |
| invalid_base | 400 | A well-formed code that we do not publish, such as XXX. |
| unsupported_parameter | 400 | A parameter this endpoint does not accept; error.parameter names it. |
| rate_limited | 429 | A window is spent — per client on the open endpoint, per key on the keyed one. retry-after says when it frees, and a refused call spends no balance. |
| request_too_long | 414 | The request URL exceeded 2,048 characters. Narrow the query. |
| service_busy | 503 | The store could not be read in time. retry-after says when to call again. |
200 · keyed response
{
"ok": true,
"endpoint": "/api/v1/rates",
"generated_at": "2026-10-30T21:07:14.512Z",
"meta": {
"base_currency": "USD",
"currency_count": 166,
"snapshot_count": 12,
"snapshot_limit": 2000,
"updated_at": "2026-10-30T20:15:04.000Z",
"tier": "account",
"quota": { "limit": 5000, "used": 148, "remaining": 4852 }
},
"data": {
"latest": {
"base_currency": "USD",
"updated_at": "2026-10-30T20:15:04.000Z",
"currency_count": 166,
"rates": { "EUR": 0.9142, "GBP": 0.7861 }
},
"currencies": [{ "code": "EUR", "name": "Euro", "symbol": "€", "flag": "…", "rate": 0.9142 }],
"snapshots": [{ "fetched_at": "2026-10-30T20:15:04.000Z", "rates": { "EUR": 0.9142 } }]
}
}429 · public window spent
{
"ok": false,
"error": {
"code": "rate_limited",
"message": "The public endpoint is limited to 5 per second, 50 per hour, 50 per day.",
"limit": { "window": "second", "limit": 5, "reset_at": "2026-10-30T21:07:15.000Z" }
}
}Code samples
Copy any of these. Replace the key placeholder with your own secret.
curl · open endpoint
curl -s "https://fiatagent.base44.app/api/v1/public/rates"curl · keyed endpoint
curl -s "https://fiatagent.base44.app/api/v1/rates?base=EUR&snapshots=48" \
-H "Authorization: Bearer fiatagent_…"JavaScript · fetch
const res = await fetch(
"https://fiatagent.base44.app/api/v1/rates?snapshots=24",
{ headers: { Authorization: "Bearer " + process.env.FIAT_AGENT_KEY } },
);
if (res.status === 429) {
throw new Error("Rate limited — retry in " + res.headers.get("retry-after") + "s");
}
if (!res.ok) throw new Error("Fiat Agent request failed: " + res.status);
const { meta, data } = await res.json();
console.log(meta.currency_count, "currencies ·", meta.quota.remaining, "requests left");
console.log("EUR", data.latest.rates.EUR);Python · requests
import os
import requests
res = requests.get(
"https://fiatagent.base44.app/api/v1/rates",
params={"base": "EUR", "snapshots": 24},
headers={"Authorization": f"Bearer {os.environ['FIAT_AGENT_KEY']}"},
timeout=5,
)
res.raise_for_status()
payload = res.json()
print(payload["meta"]["quota"]["remaining"], "requests left")
print(payload["data"]["latest"]["rates"]["USD"])Versioning
The version lives in the path. What is published here stays compatible.
- New fields can appear on a response; ignored keys are always safe to parse.
- A field is never renamed or removed inside v1. A breaking change ships as a new path.
- Additive changes need no version bump — pin nothing you do not have to.
Ready to call it?
A new account starts with 5,000 requests included — and no rate limit on keyed calls.