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

Canonical: https://currency-core.com/docs/ai-quickstart/

---
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](/docs/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.

<CodeTabs>
<Fragment slot="curl">

```bash
curl -G https://api.currency-core.com/v1/ai \
  --data-urlencode "q=Convert 50 euro to indian rupees" \
  -H "Authorization: Bearer $CURRENCYCORE_KEY"
```

</Fragment>
<Fragment slot="js">

```js
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);
```

</Fragment>
<Fragment slot="python">

```python

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"])
```

</Fragment>
</CodeTabs>

## Annotated response

```json
{
  "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](/docs/convert-endpoint/).

```bash
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"
```

```json
{
  "from": "EUR",
  "amount": 50,
  "date": "2026-11-10",
  "results": [
    { "to": "INR", "result": 5255, "rate": 105.1 }
  ]
}
```

A TypeScript check you can adapt:

```ts
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](/docs/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:

```ts
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](/docs/ai-prompt-guide/): prompts that map to each result
  shape, plus a bad-prompt to better-prompt table.
- [AI response schema](/docs/ai-response-schema/): TypeScript types for all 12
  `data` shapes and a safe rendering pattern.
- [AI grounding](/docs/ai-grounding/): why `data` is canonical and how to verify
  each shape against REST.
- [AI errors](/docs/ai-errors/): the full error matrix, retry policy, and support
  checklist.
- [BYOK](/docs/byok/): run `/v1/ai` on your own model provider on Growth+.
- [AI credits](/docs/ai-credits/): how the separate credit pool works.
