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> or x-api-key: <key>. A Free organization can use a cc_test_ key.
  • AI credits on your plan. Native /v1/ai consumes 1 AI credit per answer, including a native 422 refusal. 413 and 503 consume 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"

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
}
FieldWhat it tells you
queryThe trimmed question you sent.
answerPlain text for a human. It has no Markdown and no citations.
data[0].resultThe converted amount. Use this for calculations or display.
data[0].rateTarget units per one source unit. Use this to show the rate.
data[0].asOfThe rate’s reference date. It can be earlier than today on a weekend or holiday.
toolCallsHow many billable tool calls produced the answer. With BYOK, this is also the number of API requests billed.
byokOmitted 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 receiveWhat it meansWhat to do
200, non-empty dataA normal answer with structured results.Render answer, and use data for numbers.
200, data: [], answer asks for a missing amount or countryA 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 unavailableIn-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 largeThe 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 upgradeA Free or Starter organization asked for a Growth+ capability.Upgrade, or ask a single-date question instead.
422 unsupported_queryOff-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_inputThe question or total input is over budget.Shorten q. No credit is consumed.
429 rate_limitedPer-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_errorNo 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 data shapes and a safe rendering pattern.
  • AI grounding: why data is canonical and how to verify each shape against REST.
  • AI errors: the full error matrix, retry policy, and support checklist.
  • BYOK: run /v1/ai on your own model provider on Growth+.
  • AI credits: how the separate credit pool works.