# Authentication (/docs/authentication)

## API keys [#api-keys]

Create keys in the dashboard under **Settings, API Keys and Webhooks**. Each key belongs to one environment:

| Key prefix | Environment | Moves real money |
| --- | --- | --- |
| `ck_test_` | Test (sandbox) | No |
| `ck_live_` | Live | Yes |

A key comes with an **API secret** that is shown once. Keep both on your server. If a secret leaks, revoke the key in the dashboard and create a new one.

## Sending the key [#sending-the-key]

Send the key and secret as headers on every request, over HTTPS:

```bash
curl https://api.creddot.com/api/v1/wallets \
  -H "x-api-key: ck_test_..." \
  -H "x-api-secret: ..."
```

`Authorization: Bearer <secret>` works in place of `x-api-secret` if your HTTP client prefers it.

### Signed requests (optional) [#signed-requests-optional]

Instead of sending the secret, you can sign each request with it. This keeps the secret off the wire entirely:

| Header | Value |
| --- | --- |
| `x-api-key` | your key |
| `x-timestamp` | Unix time in **milliseconds**; must be within 5 minutes of Creddot's clock |
| `x-signature` | hex HMAC-SHA256, keyed with your secret, of the string below |

```txt
{timestamp}.{METHOD}.{path and query}.{idempotency key or empty}.{hex HMAC-SHA256 of the JSON body, keyed with your secret}
```

```js title="Node.js"
import { createHmac } from 'node:crypto';

const hmac = (data) => createHmac('sha256', process.env.CREDDOT_API_SECRET).update(data).digest('hex');

function signedHeaders(method, path, body, idempotencyKey = '') {
  const timestamp = Date.now().toString();
  const bodyHash = hmac(JSON.stringify(body ?? {}));
  const signature = hmac([timestamp, method.toUpperCase(), path, idempotencyKey, bodyHash].join('.'));
  return { 'x-api-key': process.env.CREDDOT_API_KEY, 'x-timestamp': timestamp, 'x-signature': signature, ...(idempotencyKey ? { 'x-idempotency-key': idempotencyKey } : {}) };
}
// path includes /api/v1, e.g. signedHeaders('POST', '/api/v1/payouts', body, 'payout-881')
```

<Callout type="warn">Sign the exact JSON you send. Serialise the body once and reuse the same string for the hash and the request.</Callout>

## Permissions [#permissions]

Each key only does what its permissions allow. Give every integration the smallest set it needs. A request without the right permission gets `403`.

| Permission | Allows |
| --- | --- |
| `collections:create` | Checkout sessions, server-side collections |
| `transactions:read` | Transaction lookup and listing |
| `wallets:read` | Wallet balances |
| `virtual-accounts:read` | Dedicated collection accounts |
| `payouts:create` | Recipient validation and payouts |
| `bills:read`, `bills:create` | Bill catalogue, validation and payment |
| `payment-links:read`, `payment-links:manage` | Payment links |
| `invoices:read`, `invoices:manage` | Invoices |
| `customers:read`, `customers:manage` | Customers |
| `subscriptions:read`, `subscriptions:manage` | Plans, subscriptions and subscribers |
| `refunds:read`, `refunds:create` | Refund requests |
| `sandbox:simulate` | Test-mode simulation endpoints |

## IP allow-list [#ip-allow-list]

In the dashboard you can restrict each environment to the IP addresses of your servers. Requests from other addresses are rejected with `401`, even with a valid key.

## Rate limits [#rate-limits]

Each client can make about 120 requests a minute. Above that you get `429`; wait and retry with backoff.
