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

# API keys

> Issue, list, and revoke server-to-server credentials.

API keys are the credential B2B integrators present on every server-to-server request. Each key is scoped to one organization; tenancy is locked to the key at the moment of authentication and cannot be overridden via headers.

## Wire format

```
pk_<mode>_<id>.<secret>
```

* `<mode>` is `test` or `live`. Test keys run the full happy path against a sandbox payment instance — KYB and buyer KYC auto-approve and no real money moves. Live keys execute against real rails. See [Environments](/api-reference/environments).
* `<id>` is the public lookup half — safe to log.
* `<secret>` is surfaced exactly once, at creation. Caratuva stores only a salted hash; the plaintext cannot be recovered. If you lose it, revoke the key and create a new one.

## Header

```
X-API-Key: pk_live_KEY_ID.KEY_SECRET
```

## Issue a key

```bash theme={null}
curl -X POST https://api.caratuva.com/v1/api-keys \
  -H "Authorization: Bearer <dashboard-jwt>" \
  -H "content-type: application/json" \
  -d '{
    "name": "ERP integration",
    "mode": "live",
    "scopes": [
      "payment_intents:write",
      "payment_intents:read",
      "webhooks:read"
    ]
  }'
```

`scopes` is optional. Omitting it (or passing `[]`) issues an unrestricted key — useful for the bootstrap key but not recommended for narrowly-scoped integrations. See [Scopes](#scopes) below for the available values.

Response (the `secret` field is the only place the full key ever appears):

```json theme={null}
{
  "id": "ckxyz...",
  "orgId": "ckorg_...",
  "name": "ERP integration",
  "keyPrefix": "pk_live_KEY_ID",
  "testMode": false,
  "scopes": [
    "payment_intents:write",
    "payment_intents:read",
    "webhooks:read"
  ],
  "lastUsedAt": null,
  "revokedAt": null,
  "createdAt": "2026-05-02T14:00:00.000Z",
  "secret": "pk_live_KEY_ID.KEY_SECRET"
}
```

Store the `secret` in your secret manager immediately. Subsequent `GET /v1/api-keys` calls return the metadata only — never the secret.

## List keys

```bash theme={null}
curl https://api.caratuva.com/v1/api-keys \
  -H "Authorization: Bearer <dashboard-jwt>"
```

## Revoke a key

```bash theme={null}
curl -X DELETE https://api.caratuva.com/v1/api-keys/<id> \
  -H "Authorization: Bearer <dashboard-jwt>"
```

Revocation is irreversible and takes effect on the next request. Issue a fresh key first, deploy it to your integrator, then revoke the old one to avoid downtime.

## Rotation

There is no in-place rotation endpoint for API keys (unlike webhook secrets). The rotation procedure is:

1. `POST /v1/api-keys` to create the new key.
2. Deploy the new key to your integrator.
3. `DELETE /v1/api-keys/<old-id>` to revoke.

The dashboard surfaces `lastUsedAt` so you can confirm the new key is in use before revoking the old one.

## Scopes

API keys are gated by scope on a per-route basis. A request with a scoped key that's missing the required scope returns `403 InsufficientScope`. Available scopes:

| Scope                      | Grants                                                                |
| -------------------------- | --------------------------------------------------------------------- |
| `payment_intents:write`    | `POST /v1/payment-intents`, cancel                                    |
| `payment_intents:read`     | `GET /v1/payment-intents/:id`                                         |
| `invoices:write`           | `POST /v1/invoices`, cancel, invite-buyer                             |
| `invoices:read`            | `GET /v1/invoices`, `GET /v1/invoices/:id`                            |
| `webhooks:write`           | `POST /v1/webhook-subscriptions`, delete, rotate-secret               |
| `webhooks:read`            | `GET /v1/webhook-subscriptions`, deliveries                           |
| `api_keys:write`           | `POST /v1/api-keys`, revoke                                           |
| `connected_accounts:write` | `POST /v1/connected-accounts`, tos, upload-url, submit, payout-config |
| `connected_accounts:read`  | `GET /v1/connected-accounts`, `GET /v1/connected-accounts/:id`        |

Recommended starting set for a B2B integration that creates intents and reconciles via webhooks:

```json theme={null}
[
  "payment_intents:write",
  "payment_intents:read",
  "webhooks:read"
]
```

(Note: webhook-subscription management is best done from the dashboard, not the integrator's runtime — so `webhooks:write` is usually unnecessary.)

### Backwards-compat note

Keys issued before scopes existed have `scopes = []`, which Caratuva treats as **unrestricted** to preserve their existing behavior. To migrate, issue a new key with explicit scopes, deploy, then revoke the unscoped one.

## Test vs live

Use `pk_test_*` keys against staging and CI environments. Test keys are flagged on the authenticated principal as `testMode: true`. The entire flow — buyer KYC, payment collection, cross-border transfer, FX, and PIX payout — runs against a sandbox payment instance: KYB and buyer KYC auto-approve and no real money moves. Live keys hit production rails; never use them outside production. See [Environments](/api-reference/environments) for the full mode model.
