Authentication
payitgo has two kinds of credentials:
| Credential | Used for | Who uses it |
|---|---|---|
| Secret API key (64 hex characters, a test key or a live key) | The payments API under /v1 |
Your server |
| Access token (a JWT from sign-in) | The dashboard API under /merchant |
People on your team, through the dashboard |
Your shop's backend only ever needs an API key. Access tokens are for the dashboard: team, keys, webhook secret rotation, balances and payouts.
API keys
Sending the key
Send the key on every /v1 request, in either header:
Authorization: Bearer 4f9c2a7e1b8d3f6a0c5e9b2d7a4f1c8e3b6d9a2f5c8e1b4d7a0f3c6e9b2d5a8f
X-API-Key: 4f9c2a7e1b8d3f6a0c5e9b2d7a4f1c8e3b6d9a2f5c8e1b4d7a0f3c6e9b2d5a8f
When both are present, X-API-Key wins. A complete request:
curl -s https://api.payitgo.co/v1/tokens \
-H "Authorization: Bearer $PAYITGO_KEY"
A key is 64 hex characters. Treat it as an opaque string: it does not say its mode.
Test keys and live keys
The key decides the mode of everything the request does. There is no
mode parameter on /v1. You choose the mode when you create a key, and the
dashboard shows it as a Test or Live badge next to the key.
| Key | Mode | What it does |
|---|---|---|
| Test key | Test | Test payments, paid by the outcomes you simulate (no real money), the test webhook and its signing secret |
| Live key | Live | Real payments on mainnet, the live webhook and its signing secret |
To ask payitgo which mode a key is in, call GET /v1/webhook with it: the
answer's livemode is the key's mode (Webhooks).
GET /v1/tokenslists the tokens of the key's mode.POST /v1/paymentscreates a payment in the key's mode.GET /v1/paymentsandGET /v1/payments/{id}only see payments of the key's mode. A test key gets404for a live payment, and the other way round./v1/webhookreads and changes the webhook of the key's mode. Each mode has its own URL and its own signing secret (Webhooks).
Every payment and every webhook notification carries "livemode": true or
false, so your code can always tell which mode it is looking at.
Live mode is off until the owner turns it on
A new account starts in test mode. Until the account owner turns live mode on, every request with the live key is refused:
HTTP/1.1 403 Forbidden
{"error": "live mode is off for this account: the owner can turn it on in the dashboard; until then use a test key"}
The owner turns it on with POST /merchant/live (see
Account and team). Turning it off again stops new
live payments; payments already open still complete.
Storefront keys
A key can belong to a store (one of your shops, see Stores). A storefront key works like any other key, with these differences:
-
Every payment it creates carries the store's
store_id. -
It only sees its own store's payments:
GET /v1/paymentslists that store's payments (itsstore_idfilter is ignored), andGET /v1/payments/{id}answers404 {"error": "payment not found"}for another store's payment. -
It can read the webhook (
GET /v1/webhook) and send a test (POST /v1/webhook/test), but it cannot change the account's webhook URL.PUTandDELETE /v1/webhookanswer:HTTP/1.1 403 Forbidden {"error": "a storefront's key cannot change the account's webhook: set webhook_url on its payments instead"}A store that runs its own backend sets
webhook_urlon each payment it creates, and verifies the notifications with the same signing secret (per-payment webhook URL).
Read-only keys
A key created with "access": "read" can only read: GET /v1/payments,
GET /v1/payments/{id}, GET /v1/tokens and GET /v1/webhook. Anything
else answers:
HTTP/1.1 403 Forbidden
{"error": "this api key is read-only: it can only read (GET)"}
Give one to a service that reports on payments (an accounting export, a mobile app's order history) and should never create them.
API key errors
| Status | Body | Cause |
|---|---|---|
401 |
{"error": "missing api key"} |
Neither Authorization: Bearer … nor X-API-Key was sent |
401 |
{"error": "invalid api key"} |
The key is unknown, mistyped or revoked |
403 |
{"error": "live mode is off for this account: …"} |
A live key while live mode is off |
403 |
{"error": "merchant account is suspended"} |
payitgo suspended the account |
403 |
{"error": "this api key is read-only: it can only read (GET)"} |
A read-only key used to create or change something |
429 |
{"error": "rate limit exceeded"} |
More than 20 requests a second with one key (limits) |
Managing keys
Your first test key and live key are issued at signup. Owners and developers can create more (one per environment or service, say) and revoke old ones. These routes take a dashboard access token (see below), not an API key.
The API key object
| Field | Type | Description |
|---|---|---|
id |
string | The key's id. Treat it as an opaque string |
merchant_id |
string | Your account's id |
livemode |
boolean | false for a test key, true for a live key: the only way to tell them apart |
store_id |
string | The store the key belongs to. Absent for an account-wide key |
label |
string | Your name for the key (default for the two signup keys) |
access |
string | full (everything /v1 offers) or read (read-only) |
last4 |
string | The last four characters of the secret, to tell keys apart |
created_by |
string | The team member who created it (their id) |
created_at |
string | When it was created (RFC 3339, UTC) |
last_used_at |
string | When it last authenticated a request. Absent until first used |
revoked_at |
string | When it was revoked. Absent while it works |
payitgo stores only a hash of the secret. The secret is returned once, when the key is created, and can never be shown again.
Create a key
POST /merchant/api-keys?mode=test · owner or developer
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mode |
query | string | no | test for a test key. Anything else, or nothing, makes a live key |
label |
body | string | no | Your name for the key |
store_id |
body | string | no | A store's id: the key becomes that store's key |
access |
body | string | no | full (the default) or read (read-only) |
curl -s 'https://api.payitgo.co/merchant/api-keys?mode=test' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"label": "staging"}'
201 Created:
{
"api_key": {
"id": "5e1a9c3d7b2f40e8a6c4d2b1",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": false,
"label": "staging",
"access": "full",
"last4": "c0da",
"created_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T09:02:33.526679611Z"
},
"secret": "e919a75364398a449f860aeadddc57fa0502145a4e63959ddb33c417a48dc0da"
}
Store secret on your server now: it is not shown again.
| Status | Body | Cause |
|---|---|---|
400 |
{"error": "unknown store_id"} |
store_id is not one of your stores |
400 |
{"error": "access must be \"full\" or \"read\""} |
|
401 |
{"error": "missing token"} · {"error": "invalid or expired token"} |
No access token, or an expired one |
403 |
{"error": "insufficient permissions"} |
Your role is finance or viewer |
List keys
GET /merchant/api-keys?mode=test · owner or developer
Lists the keys of one mode, revoked ones included. Without mode=test you
get the live keys.
curl -s 'https://api.payitgo.co/merchant/api-keys?mode=test' -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"api_keys": [
{
"id": "0e7c5a3f9b1d2846a1f3c5e7",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": false,
"store_id": "6d2a9e4c1b7f30a58e2c4d91",
"label": "eu-shop",
"last4": "d05e",
"created_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T08:59:52.982Z",
"last_used_at": "2026-10-10T08:59:53.054Z"
},
{
"id": "4f2a9c1e7b3d5a08c6e1d7b2",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": false,
"label": "default",
"last4": "5a8f",
"created_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T08:57:25.904Z",
"last_used_at": "2026-10-10T08:58:44.128Z"
}
]
}
Revoke a key
DELETE /merchant/api-keys/{id} · owner or developer
The key stops working at once: its next request gets
401 {"error": "invalid api key"}.
curl -s -X DELETE https://api.payitgo.co/merchant/api-keys/5e1a9c3d7b2f40e8a6c4d2b1 -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{"message": "api key revoked"}
An unknown id answers 404 {"error": "not found"}.
Rotating a key: create the new key, deploy it, check that requests succeed, then revoke the old one.
Dashboard access tokens
The /merchant/… routes are for people. Sign in with email and password to
get an access token:
POST /merchant/login
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | Your email address |
password |
string | yes | Your password |
curl -s https://api.payitgo.co/merchant/login \
-H 'Content-Type: application/json' \
-d '{"email": "you@acme.example", "password": "a-long-password"}'
200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJraW5k…",
"expires_at": "2026-10-10T09:17:33.92212983Z",
"role": "owner"
}
| Field | Description |
|---|---|
access_token |
Send it as Authorization: Bearer <access_token> on /merchant routes |
expires_at |
When it stops working: 15 minutes after it was issued |
role |
Your role in the account: owner, developer, finance or viewer |
The response also sets an httpOnly cookie, refresh_token, on the path
/merchant/auth. POST /merchant/auth/refresh exchanges it for a new access
token (same response as login) for up to 7 days. POST /merchant/auth/logout
ends the session ({"message": "logged out"}), and
POST /merchant/auth/logout-all ends every session of yours
({"message": "all sessions revoked"}).
| Status | Body | Cause |
|---|---|---|
400 |
{"error": "email and password are required"} |
A field is missing |
400 |
{"error": "invalid email address"} |
email is not an email address |
401 |
{"error": "invalid credentials"} |
Wrong email or password |
401 |
{"error": "no refresh token"} |
Refresh without the cookie |
401 |
{"error": "missing token"} |
A /merchant route without an access token |
401 |
{"error": "invalid or expired token"} |
An expired or malformed access token: refresh, or sign in again |
403 |
{"error": "insufficient permissions"} |
Your role may not use this route |
403 |
{"error": "merchant account is suspended"} |
payitgo suspended the account |
429 |
{"error": "rate limit exceeded"} |
More than 5 sign-in attempts a second from one IP address |
Keep keys secret
- Use secret keys from your server only. Never put them in a browser, a mobile app, a public repository or a support ticket. The checkout routes your customers' browsers call need no key (Checkout).
- If a key may have leaked, revoke it at once. An API key cannot withdraw
money (payouts need a signed-in team member with the
ownerorfinancerole), but it can create payments and change your webhook URL in your name. - A lost secret cannot be shown again: create a new key and revoke the lost one.