AI
AI Quickstart
Make your first GET /v1/ai request, read the response, verify the number against /v1/convert, and handle a 200 that is not a direct answer.
This page takes you from an API key to a verified answer in one request. The AI endpoint returns a plain-text answer plus the structured result behind it, so you can display the prose and still check the number in code.
Before you start
You need:
- A CurrencyCore API key. Send it as
Authorization: Bearer <key>orx-api-key: <key>. A Free organization can use acc_test_key. - AI credits on your plan. Native
/v1/aiconsumes 1 AI credit per answer, including a native422refusal.413and503consume none. See AI credits. - Growth or higher for historical trend, comparison, and analytics questions. Conversion, single rates, multi-rates, and PPP conversions work on every plan with AI credits.
One request
The question is a URL query parameter. Percent-encode it, or let your HTTP client do it.
curl -G https://api.currency-core.com/v1/ai \
--data-urlencode "q=Convert 50 euro to indian rupees" \
-H "Authorization: Bearer $CURRENCYCORE_KEY"const url = new URL("https://api.currency-core.com/v1/ai");
url.searchParams.set("q", "Convert 50 euro to indian rupees");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CURRENCYCORE_KEY}` },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (!res.ok) {
throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
}
console.log(body.answer);import os
import requests
res = requests.get(
"https://api.currency-core.com/v1/ai",
params={"q": "Convert 50 euro to indian rupees"},
headers={"Authorization": f"Bearer {os.environ['CURRENCYCORE_KEY']}"},
timeout=30,
)
res.raise_for_status()
body = res.json()
print(body["answer"])Annotated response
{
"query": "Convert 50 euro to indian rupees",
"answer": "50 EUR = 5,255.00 INR at the 2026-11-10 reference rate.",
"data": [
{
"from": "EUR",
"to": "INR",
"amount": 50,
"result": 5255,
"rate": 105.1,
"pppApplied": false,
"asOf": "2026-11-10"
}
],
"toolCalls": 1
}
| Field | What it tells you |
|---|---|
query | The trimmed question you sent. |
answer | Plain text for a human. It has no Markdown and no citations. |
data[0].result | The converted amount. Use this for calculations or display. |
data[0].rate | Target units per one source unit. Use this to show the rate. |
data[0].asOf | The rate’s reference date. It can be earlier than today on a weekend or holiday. |
toolCalls | How many billable tool calls produced the answer. With BYOK, this is also the number of API requests billed. |
byok | Omitted unless BYOK is active. true means your provider answered; false means a native fallback did. |
The example values are illustrative. Do not cache this exact payload or treat the rates as fixed.
Verify the number against /v1/convert
/v1/ai does not cite its source per number. The reliable pattern is to take the
data object and re-run the same calculation against the matching REST endpoint.
For a conversion, pass the same from, to, amount, and asOf date to
/v1/convert.
curl -G https://api.currency-core.com/v1/convert \
--data-urlencode "from=EUR" \
--data-urlencode "to=INR" \
--data-urlencode "amount=50" \
--data-urlencode "date=2026-11-10" \
-H "Authorization: Bearer $CURRENCYCORE_KEY"
{
"from": "EUR",
"amount": 50,
"date": "2026-11-10",
"results": [
{ "to": "INR", "result": 5255, "rate": 105.1 }
]
}
A TypeScript check you can adapt:
type ConvertDatum = {
from: string;
to: string;
amount: number;
result: number;
rate: number;
pppApplied: boolean;
asOf: string;
};
export async function verifyConvert(
datum: ConvertDatum,
key: string,
): Promise<{ ok: boolean; restResult: number; restRate: number }> {
// PPP conversions need the country-aware /v1/convert?ppp=true form.
if (datum.pppApplied) throw new Error("Verify PPP conversions against /v1/convert?ppp=true");
const url = new URL("https://api.currency-core.com/v1/convert");
url.searchParams.set("from", datum.from);
url.searchParams.set("to", datum.to);
url.searchParams.set("amount", String(datum.amount));
url.searchParams.set("date", datum.asOf);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (!res.ok) throw new Error(`Verification failed: HTTP ${res.status}`);
const body = await res.json();
const row = body.results.find((r: { to: string }) => r.to === datum.to);
if (!row) throw new Error(`No ${datum.to} result in /v1/convert response`);
const tolerance = 0.0001;
const ok =
Math.abs(row.result - datum.result) <= Math.abs(datum.result) * tolerance &&
Math.abs(row.rate - datum.rate) <= Math.abs(datum.rate) * tolerance;
return { ok, restResult: row.result, restRate: row.rate };
}
The same principle applies to every shape: re-run the matching REST endpoint with
the fields from the data object. Coverage for each shape and a history-analysis
check are on AI grounding.
Handle a 200 that is not a direct answer
Not every 200 contains a conversion. The current response has no outcome or
kind field, so branch on the HTTP status first, then on data and answer.
| What you receive | What it means | What to do |
|---|---|---|
200, non-empty data | A normal answer with structured results. | Render answer, and use data for numbers. |
200, data: [], answer asks for a missing amount or country | A clarification is needed. The model did not guess. | Ask the user for the detail and send a new request. |
200, data: [], answer explains that data is unavailable | In-scope question, no data for that currency, date, country, or year. | Show the explanation. Do not retry the same question. |
200, data: [], answer says the analysis is too large | The orchestration deadline was hit without a usable analysis. toolCalls is 0. | Narrow the window or ask about fewer currencies. |
200, data: [], answer asks you to upgrade | A Free or Starter organization asked for a Growth+ capability. | Upgrade, or ask a single-date question instead. |
422 unsupported_query | Off-topic question or prompt injection. The model was consulted. | Do not retry. Reformulate as a currency, FX, or PPP question. A native 422 consumes 1 credit. |
413 invalid_input | The question or total input is over budget. | Shorten q. No credit is consumed. |
429 rate_limited | Per-minute limit, monthly request quota, or AI credits exhausted. | Honor Retry-After when present. For exhausted AI credits, buy an add-on or wait for the next cycle. |
503 internal_error | No provider available, every provider unreachable, or BYOK required with fallback off. | Retry with backoff for a transient outage. Fix BYOK configuration if it persists. No credit is consumed. |
A helper that does the safe thing:
async function ask(q: string, key: string) {
const url = new URL("https://api.currency-core.com/v1/ai");
url.searchParams.set("q", q);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (!res.ok) {
// Do not retry 4xx except 429. Retry 5xx with backoff.
return { state: "error" as const, status: res.status, code: body.error?.code };
}
if (body.data.length === 0) {
// Clarification, no-data explanation, upgrade prompt, or too-large analysis.
return { state: "no-data" as const, answer: body.answer };
}
return { state: "answer" as const, answer: body.answer, data: body.data };
}
Do not invent an outcome field to make this easier. It is not in the current
API. If you need a stable local branch, derive one from data.length and the HTTP
status, as above.
Next steps
- AI prompt guide: prompts that map to each result shape, plus a bad-prompt to better-prompt table.
- AI response schema: TypeScript types for all 12
datashapes and a safe rendering pattern. - AI grounding: why
datais canonical and how to verify each shape against REST. - AI errors: the full error matrix, retry policy, and support checklist.
- BYOK: run
/v1/aion your own model provider on Growth+. - AI credits: how the separate credit pool works.