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

FieldTypeNotes
GET /api/v1/public/ratesNo keyLatest rates and the currency catalogue · Limited per client
GET /api/v1/ratesBearer keyLatest 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.

  1. 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.

    Create account
  2. 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.

    Open API screen
  3. 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 key

Open 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 key

The 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

FieldTypeNotes
?basestringA 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.
?snapshotsintegerHow many stored readings to return, 1–2,000. Defaults to 2,000. Keyed endpoint only.
?fromdateFirst day of stored history to return, written YYYY-MM-DD in UTC and inclusive. Keyed endpoint only.
?todateLast 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

FieldTypeNotes
?basestringA 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.
?snapshotsintegerHow many stored readings to return, 1–2,000. Defaults to 2,000. Keyed endpoint only.
?fromdateFirst day of stored history to return, written YYYY-MM-DD in UTC and inclusive. Keyed endpoint only.
?todateLast 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

FieldTypeNotes
base_currencystringCurrency every rate in this response is quoted against.
currency_countintegerHow many currency codes the response carries.
updated_atISO 8601When we read these rates from the source. Never a promise about how fresh the source's own figures are.
freshnessobjectfetched_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.
tierstring"public" on the open endpoint.
limitsarrayThe three published windows, as window and limit pairs.

meta · keyed endpoint

FieldTypeNotes
base_currencystringCurrency every rate in this response is quoted against.
currency_countintegerCurrency codes in the response.
snapshot_countintegerReadings included in data.snapshots.
snapshot_limitintegerMost readings one response can carry.
updated_atISO 8601When we read these rates from the source.
freshnessobjectfetched_at, source_updated_at, age_minutes and stale — the same block the open endpoint returns.
tierstring"account" on a keyed call.
limitsarrayThis key's windows, as window and limit pairs.
quotaobjectThe account's request balance — see the quota fields below.

data.latest

FieldTypeNotes
base_currencystringBase these rates are quoted against.
updated_atISO 8601When we read the reading these rates came from.
source_updated_atISO 8601When the underlying source last published rates. Equal to updated_at only when the source republishes on every read.
age_minutesintegerHow many minutes old the stored reading is, counted from updated_at.
currency_countintegerCodes in rates.
ratesobjectMap of currency code to quote against base_currency.

data.currencies[]

FieldTypeNotes
codestringISO 4217 code as published.
namestringOfficial currency name.
symbolstringDisplay symbol; the code itself when the currency has none.
flagstring | nullFlag image URL, 80px wide, or null for codes with no single territory.
ratenumber | nullQuote against base_currency, or null when no quote is held.

data.snapshots[] · keyed endpoint

FieldTypeNotes
fetched_atISO 8601When the reading entered our store. Sorted newest first.
base_currencystringBase this reading is reported against — always the requested base.
updated_atISO 8601Timestamp of the reading itself.
currency_countintegerCodes in rates.
ratesobjectMap of currency code to quote.

meta.quota · keyed endpoint

FieldTypeNotes
limitintegerTotal balance: your signup bonus plus every pack bought.
usedintegerKeyed requests spent all time, across every key on the account.
remainingintegerRequests 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

Buy

From $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

FieldTypeNotes
per second5 requestsRolling UTC window. The call that would exceed it answers 429.
per hour50 requestsRolling UTC window. The call that would exceed it answers 429.
per day50 requestsRolling UTC window. The call that would exceed it answers 429.

Keyed endpoint · per key

FieldTypeNotes
per key, per second20 requestsBurst protection, set far above working load. A refused call draws nothing from the balance.
per key, per hour2000 requestsBurst 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.
Buy a top-up

Headers

Limits are readable from every response, so a client can pace itself without waiting for a 429.

Open endpoint

FieldTypeNotes
x-ratelimit-limitintegerRequests allowed in the tightest window for this client.
x-ratelimit-remainingintegerRequests left in that window after this call.
x-ratelimit-resetsecondsUntil that window rolls over.
access-control-allow-origin*Every response is readable from a browser.
cache-controlno-storeResponses are never cached by a proxy.

Keyed endpoint

FieldTypeNotes
x-quota-limitintegerThe account's total balance.
x-quota-remainingintegerRequests left in the balance after this call.
x-ratelimit-limitintegerRequests allowed in the tightest window for this key.
x-ratelimit-remainingintegerRequests left in that window after this call.
x-ratelimit-resetsecondsUntil that window rolls over.
retry-aftersecondsSent with a 503: when to call again.
access-control-allow-origin*Every response is readable from a browser.
cache-controlno-storeResponses 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

FieldTypeNotes
missing_key401A keyed call arrived without a bearer credential.
invalid_key401The key is not recognised.
revoked_key401The key was revoked by its owner and no longer works.
invalid_parameter400base, snapshots, from or to is malformed; error.parameter names it.
invalid_base400A well-formed code that we do not publish, such as XXX.
unsupported_parameter400A parameter this endpoint does not accept; error.parameter names it.
rate_limited429A 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_long414The request URL exceeded 2,048 characters. Narrow the query.
service_busy503The 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.

Create free account