---
title: Authentication
description: API keys, how to keep them safe, and how to rotate them without downtime.
group: concepts
order: 2
---

Send your key as a bearer token on every request:

```bash
curl https://api.simconnectng.com/api/v1/wallet/balance \
  -H "Authorization: Bearer $SIMCONNECT_KEY"
```

## Keys

- Test keys start with `scn_test_`, live keys with `scn_live_`. The key decides the mode: there is no separate test address.
- A key is shown once, when you create it. Store it in your environment or secret manager.
- Keep keys out of client-side code, mobile apps and version control.

A request with no key, or a key that does not exist, is refused with `401 AUTH_REQUIRED`.

## Rotating a key

Create a new key, deploy it, then revoke the old one. Both work during the overlap, so there is no downtime.

## Locking a key down

Each key can carry an IP allowlist. Requests from any other address are refused with `403 PERMISSION_DENIED`, "Client address is not allowed for this key".

## Signing requests

A key can also require signed requests. Its signing secret is shown once, with the key. Each request then carries two more headers:

| Header | Value |
| --- | --- |
| `SimConnect-Timestamp` | The current unix time in seconds |
| `SimConnect-Signature` | Lower case hex HMAC SHA-256 of `<timestamp>.<raw body>`, keyed with the signing secret |

```js
import crypto from "node:crypto";

const body = JSON.stringify({ product_code: "MTN-VTU", amount: 500, phone_number: "08012345678" });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto.createHmac("sha256", process.env.SIMCONNECT_SIGNING_SECRET).update(`${timestamp}.${body}`).digest("hex");
// Send body exactly as signed, with SimConnect-Timestamp: timestamp and SimConnect-Signature: signature.
```

Sign the exact bytes you send; a `GET` signs an empty body. A timestamp more than five minutes from our clock, or a signature that does not match, is refused with `401 AUTH_REQUIRED`, "Request signature missing or invalid". The IP allowlist is checked first.
