Integrations
Multi-Currency Invoicing
Price an invoice at a fixed date, store the rate and response metadata, and reproduce the same calculation for audit.
The job
Issue an invoice in one currency and report or settle it in another. Price the invoice once, at the invoice date, and keep enough of the response to prove how the number was produced months later.
Use /v1/convert for the amount,
/v1/rates for a dated snapshot, and
/v1/history for the series behind a prior
calculation. CurrencyCore does not create invoices, calculate tax, or send
documents.
The data model
Store the original amount, the applied rate, and the date the rate actually came from. Do not derive the rate again when the invoice is displayed.
| Field | Example | Why it matters |
|---|---|---|
invoice_id | INV-2026-0042 | Ties the rate to one document |
invoice_date | 2026-11-10 | The business date being priced |
from_currency | USD | The invoice currency |
to_currency | EUR | The reporting or settlement currency |
source_amount | 12500.00 | The pre-conversion total |
rate | 0.9214 | The exact factor applied |
converted_amount | 11517.50 | The amount shown on the document |
rate_date | 2026-11-10 | The snapshot date actually used |
requested_date | 2026-11-10 | The date you asked for, when it differs |
as_of | {"USD":"2026-11-06"} | Currencies carried forward, when present |
fetched_at | 2026-11-10T14:22:08Z | When your system recorded the response |
A useful table definition:
create table invoice_fx (
invoice_id text primary key,
invoice_date date not null,
from_currency char(3) not null,
to_currency char(3) not null,
source_amount numeric(18,2) not null,
rate numeric(18,8) not null,
converted_amount numeric(18,2) not null,
rate_date date not null,
requested_date date,
as_of jsonb,
fetched_at timestamptz not null default now()
);
Price once, at the invoice date
Calculate the total at the invoice date, store the result, and stop. Do not recalculate when the invoice is viewed, approved, or paid. A later rate is a different economic event, not a correction.
The API takes the request date as a UTC calendar date. When that date has no
snapshot, it resolves to the most recent earlier date and reports both values:
date is the date actually used, and requestedDate appears when it differs.
asOf lists individual currencies whose value was carried forward. Store all
three.
A complete pricing function
This function runs server-side, validates the response, and returns a row ready for the table above. It uses a test key during development and a live key on a paid plan.
type ConvertResponse = {
from: string;
amount: number;
date: string;
requestedDate?: string;
asOf?: Record<string, string>;
results: { to: string; result: number; rate: number }[];
};
type InvoiceRate = {
invoiceId: string;
invoiceDate: string;
from: string;
to: string;
sourceAmount: number;
rate: number;
convertedAmount: number;
rateDate: string;
requestedDate?: string;
asOf?: Record<string, string>;
fetchedAt: string;
};
async function fetchConversion(
from: string,
to: string,
amount: number,
date: string,
): Promise<ConvertResponse> {
const url = new URL("https://api.currency-core.com/v1/convert");
url.searchParams.set("from", from);
url.searchParams.set("to", to);
url.searchParams.set("amount", String(amount));
url.searchParams.set("date", date);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CURRENCECORE_API_KEY}` },
signal: AbortSignal.timeout(8000),
});
const body = await res.json();
if (!res.ok) {
throw Object.assign(new Error(body.error?.message ?? `HTTP ${res.status}`), {
status: res.status,
code: body.error?.code,
});
}
return body as ConvertResponse;
}
export async function priceInvoice(input: {
invoiceId: string;
invoiceDate: string;
from: string;
to: string;
sourceAmount: number;
}): Promise<InvoiceRate> {
const response = await fetchConversion(
input.from,
input.to,
input.sourceAmount,
input.invoiceDate,
);
const target = response.results.find((row) => row.to === input.to);
if (!target) throw new Error(`No ${input.to} result in the response`);
return {
invoiceId: input.invoiceId,
invoiceDate: input.invoiceDate,
from: input.from,
to: input.to,
sourceAmount: input.sourceAmount,
rate: target.rate,
convertedAmount: target.result,
rateDate: response.date,
requestedDate: response.requestedDate,
asOf: response.asOf,
fetchedAt: new Date().toISOString(),
};
}
What the response tells you
A priced invoice for 12,500 USD on 2026-11-10 returns:
{
"from": "USD",
"amount": 12500,
"date": "2026-11-10",
"results": [{ "to": "EUR", "result": 11517.5, "rate": 0.9214 }]
}
| Field | Use |
|---|---|
results[].rate | Store the exact factor, not a rounded display value |
results[].result | The converted amount before your rounding policy |
date | The snapshot date actually used |
requestedDate | Present only when the requested date had no snapshot |
asOf | Present only when at least one rate was carried forward |
If you request a Saturday, date can be the preceding Friday and
requestedDate will be the Saturday. That is normal for a daily fixing. Do not
treat it as an error, and do not silently replace the invoice date with the rate
date. Store both so the audit trail shows the business date and the data date.
Rounding and storage rules
- Store
ratewith at least eight decimal places. Do not store the rounded display value as the source of truth. - Round the converted amount once, at the boundary where the invoice is issued, using a documented rule such as half-up to the currency’s minor unit.
- Keep the original source amount. Do not recalculate it from the converted amount and the rounded rate.
- For zero-decimal currencies such as JPY and KRW, round to zero decimals. Do not assume every currency has two.
- If your accounting system requires a fixed rate with a fixed number of decimals, store both the raw rate and the posted rate.
Reproduce the invoice for audit
For an audit, issue the identical request with the original date and compare
results[].rate. The response is deterministic for a stored snapshot.
/v1/history is available on every plan, but Free is limited to the last 30
days and older dates return 403 forbidden. On Starter and above you can
retrieve the full history. If you are on Free, retain the original response
alongside the invoice so old documents remain reproducible after the window
closes.
Failure handling
| Status | What to do |
|---|---|
400 invalid_input | Fix the currency, amount, or date. Do not retry unchanged |
401 unauthorized | Check the key and the Bearer prefix |
403 forbidden | A Free key is asking for a date outside the 30-day window, or a plan gate blocks the call |
429 rate_limited | Honor Retry-After. Per-minute limits count every request, including errors |
5xx | Retry with backoff. Do not issue a new rate for the same invoice |
The monthly quota counts only successful billable requests. The per-minute limit counts every request, including retries and errors. See rate limits for the plan table.
Worked example
An invoice dated 2026-11-10 for 12,500 USD, reported in EUR:
- Request
/v1/convert?from=USD&to=EUR&amount=12500&date=2026-11-10. - Receive
rate = 0.9214andresult = 11517.5. - Store the raw rate, the converted amount,
date, andfetched_at. - Round the posted amount once, for example to
11517.50. - On review, re-run the same request and compare the rate. If the stored value and the new value differ, keep both and flag the later observation rather than overwriting history.