> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kori.ml/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Exchange your API key and secret for a scoped Bearer token.

The Merchant API uses a two-step credential model:

1. **API key + secret** — long-lived credentials tied to your merchant account. Created
   in the dashboard's **API Keys** screen. Never expose the secret in client-side code.
2. **Bearer token** — a short-lived JWT you obtain from the key + secret. It embeds a
   set of [scopes](/scopes) and is sent on every API call.

## Get a token

<ParamField header="X-API-Key" type="string" required>
  Your API key, e.g. `mk_...`.
</ParamField>

<ParamField header="X-API-Secret" type="string" required>
  Your API secret, e.g. `ms_...`.
</ParamField>

```bash theme={null}
curl -X POST https://api.kori.ml/merchant/api/auth \
  -H "X-API-Key: mk_your_api_key" \
  -H "X-API-Secret: ms_your_api_secret" \
  -H "Content-Type: application/json" \
  -d '{ "scopes": ["pay", "balance"], "expires_in_hours": 24 }'
```

### Body parameters

<ParamField body="scopes" type="string[]">
  Subset of your key's [scopes](/scopes) to embed in the token. Defaults to
  `["pay", "deposit", "balance"]` if omitted.
</ParamField>

<ParamField body="expires_in_hours" type="integer" default="24">
  Token lifetime in hours, between `1` and `720` (30 days).
</ParamField>

<Note>
  The `/auth` endpoint is rate limited to **20 requests per 15 minutes**. Cache and reuse
  the token until it expires rather than authenticating on every call.
</Note>

## Use the token

Send it as a Bearer token on every other endpoint:

```bash theme={null}
curl https://api.kori.ml/merchant/api/balance \
  -H "Authorization: Bearer <token>"
```

If the token is missing or invalid you get `401`. If it is valid but lacks the scope an
endpoint requires, you get `403` with the required scope named in the message.

## IP whitelisting

If you add IP addresses to your account's whitelist (dashboard → **API Keys → IP
Whitelist**), requests from any other IP are rejected with `403`, even with a valid
token. Leave the whitelist empty to allow all IPs.

## Security best practices

<AccordionGroup>
  <Accordion title="Keep secrets server-side" icon="lock">
    The API secret and Bearer tokens must never appear in browser or mobile app code.
    Make API calls from your backend.
  </Accordion>

  <Accordion title="Request least privilege" icon="shield-halved">
    Only grant a key the [scopes](/scopes) it needs, and request an even narrower set in
    the `/auth` call when appropriate.
  </Accordion>

  <Accordion title="Rotate credentials" icon="arrows-rotate">
    You can regenerate your API credentials from the dashboard at any time. Doing so
    immediately invalidates the old key.
  </Accordion>
</AccordionGroup>
