Skip to content

FX rates

Every transaction is measured in your workspace’s base currency (usually GHS), so a threshold such as “GHS 50,000 in a day” works whatever currency the money moved in. To convert, SpeculaPro needs your exchange rates. You load them through this endpoint, ideally once a day, before that day’s transactions.

If all your transactions are in your base currency, you never need this endpoint.

POST /api/v1/ingest/fx-rates
Authorization: Bearer spk_…

The body is {"items": [ … ]} with up to 500 rates.

Field Type Required What it is Example
quote 3-letter code Yes The foreign currency "USD"
as_of date YYYY-MM-DD Yes The day the rate applies from "2026-10-09"
rate string of digits, up to 12 decimals Yes How many units of your base currency one unit of quote buys "15.25"
source string, ≤ 64 No (default api) Where the rate came from, kept for audit "BoG interbank"

The base currency is always your workspace’s; you don’t send it.

rate is a string, not a JSON number, so it arrives exactly as written with no floating-point rounding: "15.25", not 15.25. It must be greater than zero.

Terminal window
curl https://speculapro.example.com/api/v1/ingest/fx-rates \
-H "Authorization: Bearer $SPECULAPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"quote": "USD", "as_of": "2026-10-09", "rate": "15.25", "source": "BoG interbank"},
{"quote": "EUR", "as_of": "2026-10-09", "rate": "17.6120", "source": "BoG interbank"},
{"quote": "XOF", "as_of": "2026-10-09", "rate": "0.02685", "source": "BoG interbank"},
{"quote": "NGN", "as_of": "2026-10-09", "rate": "0.0101", "source": "BoG interbank"}
]
}'
{
"accepted": 4, "duplicate": 0, "stale": 0, "rejected": 0, "retry": 0,
"results": [
{"index": 0, "external_id": "USD@2026-10-09", "status": "accepted"},
{"index": 1, "external_id": "EUR@2026-10-09", "status": "accepted"},
{"index": 2, "external_id": "XOF@2026-10-09", "status": "accepted"},
{"index": 3, "external_id": "NGN@2026-10-09", "status": "accepted"}
]
}

Rates have no external_id of their own; the result shows QUOTE@DATE so you can match results to what you sent.

Send the same quote and as_of again with the right rate. The new value replaces the old one for that day.

  1. Choose your rate source (for example, your treasury’s daily rates or the central bank’s published rates) and use it consistently.
  2. Schedule a job early each morning, before the day’s foreign-currency transactions arrive, that sends every currency you deal in with today’s as_of.
  3. Check the response: every rate should be accepted.
  4. Alert someone if the job fails. Without fresh rates, foreign-currency transactions are rejected after a few days (see below).

For each transaction, SpeculaPro uses the most recent rate dated on or before the transaction’s date (occurred_at, as a UTC calendar day).

  • A rate dated after the transaction is never used.
  • If the newest usable rate is more than 8 days older than the transaction (the default 7-day maximum age, SP_INGEST_FX_MAX_AGE, plus one day of grace), the transaction is rejected rather than converted with a stale rate.

So rates loaded every working day cover weekends and public holidays without any extra effort.

Message Cause Fix
quote equals the workspace base currency You sent a rate for the base currency itself (e.g. GHS in a GHS workspace) Leave it out: no conversion is needed
rate must be greater than zero A rate of zero, however it is written ("0", "0.000"). Only that rate is rejected; the others in the batch are stored Send the real rate, or leave the currency out for that day
422 for the whole request Missing field, a rate sent as a number instead of a string, a negative rate, or a date not in YYYY-MM-DD form See Responses and statuses