Authentication
Every call to the NATIO API is authenticated with a secret API key sent as a bearer token. The key alone determines the merchant, the project and whether you are in test or live mode.
Bearer API keys
Send the key in the Authorization header. There is no other authentication scheme, no signing of API requests, and no merchant or project parameter — those are resolved from the key.
curl https://api.natio.me/v1/me \
-H "Authorization: Bearer natio_sk_test_..."GET /v1/me is the cheapest way to confirm that a key works and to see exactly which merchant, project and mode it resolves to.
{
"merchant": { "id": "mer_...", "name": "Demo Merchant Ltd" },
"project": { "id": "prj_...", "name": "Default" },
"mode": "test",
"api_key": { "id": "key_...", "prefix": "natio_sk_test_AP8j", "name": "Sandbox key" }
}Test keys and live keys
A key is bound to one mode for its whole life. There is no header or parameter that switches mode; you change mode by using a different key. Test and live data are fully separated — a test key can never read a live payment and the reverse.
| natio_sk_test_… | natio_sk_live_… | |
|---|---|---|
| Providers reached | NATIO demo providers only | The licensed providers connected to your account |
| Money movement | Simulated end to end, no funds move | Real: funds move between the merchant and licensed providers |
test_scenario | Accepted on payments and refunds | Rejected with 400 test_scenario_not_allowed |
| Availability | Immediately after registration | Issued only once merchant KYB review is approved |
| Hosted pages | Sandbox simulation pages | Provider-hosted pages |
Storage, rotation and revocation
The secret is generated once and shown once. NATIO stores only a SHA-256 hash of it plus a short display prefix (for example natio_sk_test_AP8j), so a key can be identified in the dashboard and in audit logs without ever being readable again.
- Where it belongs
- A secrets manager or an environment variable on the server. Never in a browser, a mobile app, a repository or a log line.
- Creating a key
- Dashboard → API keys. Each key has a name and a mode; keys are scoped to one project.
- Rotation
- Create the new key, deploy it, confirm traffic has moved by watching the last-used timestamp on the old key, then revoke the old key. Both work at once, so there is no downtime window.
- Revocation
- Revoking is immediate and irreversible. The next request with that key returns
401 invalid_api_key. - Compromise
- Revoke first, investigate second. Every key creation and revocation is written to the audit trail with the acting user.
- Least privilege
- Use one key per deployed service rather than one shared key, so a rotation or a revocation never takes the whole estate down.
IP allow-list per project
Each project can carry an allow-list of source addresses. When the list is non-empty, any API request from an address outside it is refused before the route runs, regardless of whether the key is valid. An empty list means no IP restriction.
Entries are exact IPv4 or IPv6 addresses, or CIDR ranges such as 203.0.113.0/24 and 2001:db8::/32. Configure the list under project settings in the dashboard.
Rate limits
API traffic is rate limited per key over a rolling one-minute window. Responses carry the state of that window, so a client should read the headers rather than hardcode a number.
HTTP/1.1 201 Created
content-type: application/json; charset=utf-8
x-request-id: req_sT5KhaixXe6tNK0q
x-ratelimit-limit: 300
x-ratelimit-remaining: 293
x-ratelimit-reset: 36| Header | Meaning |
|---|---|
x-ratelimit-limit | Requests allowed in the current window. |
x-ratelimit-remaining | Requests still available in the current window. |
x-ratelimit-reset | Seconds until the window resets. |
Exceeding the window returns 429 with the standard error envelope. Back off for at least x-ratelimit-reset seconds, then retry. Because mutating requests should carry an Idempotency-Key, retrying after a 429 is safe and cannot duplicate a payment.
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests"
},
"request_id": "req_Yp3BZPrpF6EwG8WS"
}async function callNatio(path, init, attempt = 0) {
const res = await fetch(`https://api.natio.me${path}`, init);
if (res.status === 429 && attempt < 3) {
const reset = Number(res.headers.get("x-ratelimit-reset") ?? "1");
await new Promise((r) => setTimeout(r, Math.max(reset, 1) * 1000));
return callNatio(path, init, attempt + 1);
}
return res;
}Request ids
Every response carries an x-request-id header, and every error body repeats it as a top-level request_id field. The same id appears in the NATIO server logs and in the audit trail for the action, which makes it the fastest way to have a specific call investigated.
- Log
x-request-idalongside your own correlation id for every NATIO call, successful or not. - When you open a support request, quote the
request_id, the UTC timestamp and the object id (pay_…,rf_…,po_…). That triple identifies the call exactly; a description of the symptom does not. - Never quote the API key itself, or any part of it beyond the visible prefix.
401 and 403 responses
Authentication and permission failures use the same envelope as every other error: an error object with type, code and message, plus the request_id. Branch on error.code, never on the message text.
401 — authentication_error
No key was presented, or the header was not a bearer token:
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Provide your API key as `Authorization: Bearer natio_sk_test_...`"
},
"request_id": "req_pqumnNFCN09cAqT2"
}The key was presented but is unknown or has been revoked:
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Invalid or revoked API key"
},
"request_id": "req_0uwP4ioIZ8q8dgBW"
}403 — permission_error
The key is valid, but the call is not allowed from this source address:
{
"error": {
"type": "permission_error",
"code": "ip_not_allowed",
"message": "Request IP is not in the project allow-list"
},
"request_id": "req_M2XNd2ZI3qslCEU3v"
}The other 403 code is forbidden, returned when a valid key is used for something it is not entitled to — for example a test-only endpoint such as GET /v1/test/scenarios called with a live key.
| Status | Code | What to do |
|---|---|---|
401 | unauthorized | Fix the header. It must be exactly Authorization: Bearer <key>. |
401 | invalid_api_key | The key is wrong, revoked, or from the other environment. Do not retry; it will not recover. |
403 | ip_not_allowed | Add the calling egress range to the project allow-list. |
403 | forbidden | The endpoint is not available for this key — usually a test-only endpoint called with a live key. |
The full error catalogue, including the failure codes carried by failed payments, is in Errors and failure codes.