The Money API authenticates every request with two things: a bearer token that identifies your API client, and an HMAC signature that proves the request was not altered in transit. Both are required on every call. This is stricter than a bearer-only service — signing is the part unique to Money, and most of this page is about getting it right.
The examples use the sandbox base https://sandbox-api.gozem.co; production is https://api.gozem.co. All Money endpoints sit under the /money/v1 service path.
Required headers
| Header | Value |
|---|---|
Authorization |
Bearer <token> — the OAuth2 client-credentials token (see below) |
x-timestamp |
Current Unix time in seconds, as a string |
x-hmac-signature |
Hex-encoded HMAC-SHA256 of the canonical request string |
idempotency-key |
Recommended on POST /charge and POST /payout (see Idempotency) |
Step 1: Get a bearer token
Exchange your client_id and client_secret for a token using the OAuth2 client-credentials flow, then send it as Authorization: Bearer <token>. The flow, token lifetime, and rotation are covered once in Authentication (platform) — the Money API uses the same token. Where your credentials come from is covered in Environments & Access.
Step 2: Sign the request (HMAC)
Derive x-hmac-signature with the HMAC secret issued for your API client. The algorithm:
bodyString = '""' # no body (typical GET) — JSON-encoded empty string
= raw JSON string # request body, exactly as sent
bodyHash = HMAC_SHA256(key = "", message = bodyString) # hex
canonical = timestamp + "\n" + METHOD + "\n" + path + "\n" + bodyHash
signature = HMAC_SHA256(key = your_hmac_secret, message = canonical) # hex
Rules that matter:
METHODis upper case (GET,POST).pathis the service path and query string that follows the host, byte-for-byte — the leading slash, the/money/v1/...route, and the query string if any (for example/money/v1/charge?page=1&limit=20). Never include the scheme or host, and if your base URL carries a gateway prefix, leave that out too: the gateway strips it before the API verifies the signature. A dropped query string or a normalized path fails the check.- An empty body signs as
""— the two-character JSON encoding of an empty string, not a zero-length string. This is whatJSON.stringify('')produces, and it is the most common cause of401 Invalid request signatureonGETcalls. - The body hash uses an empty-string key; only the request signature uses your HMAC secret.
- Clock skew: requests are rejected if
x-timestampdiffers from Gozem server time by more than 300 seconds (the timestamp is in seconds, not milliseconds). Sign right before sending and keep your clock synced.
Example (Node.js)
const crypto = require('crypto');
function sign({ method, path, body, secret }) {
const timestamp = String(Math.floor(Date.now() / 1000));
// An absent body signs as JSON.stringify('') === '""', not as ''.
const bodyString = body ? JSON.stringify(body) : '""';
const bodyHash = crypto.createHmac('sha256', '').update(bodyString).digest('hex');
const canonical = `${timestamp}\n${method.toUpperCase()}\n${path}\n${bodyHash}`;
const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
return { 'x-timestamp': timestamp, 'x-hmac-signature': signature };
}
// path is everything after the host, e.g. "/money/v1/charge" — never the full URL
Credentials
Beyond the OAuth client_id / client_secret (see Environments & Access), a Money API client is provisioned with:
| Credential | Used for |
|---|---|
| HMAC secret | Signing requests (above). Treat it like a password; rotate on suspicion of exposure. |
| Webhook secret | Verifying inbound webhooks (see Payments Lifecycle & Events). |
| RSA public key | Optionally encrypting the pin on POST /charge and POST /payout (see PIN encryption). |
| Linked merchant account | The account charges land in and payouts debit from. |
Authorization checks
Once a request is authenticated, it passes these checks in order — the first failure is the one returned:
- Endpoint slug — your client must be entitled to the route’s slug (see below), or
403SEC_ENDPOINT_NOT_AUTHORIZED. - Scope — your client must hold the OAuth scope the operation requires.
- HMAC — signature and timestamp must validate, or
401. - Merchant linkage — your client must resolve to a merchant account with a primary wallet, or
403MERCHANT_NOT_RESOLVED/400MERCHANT_ACCOUNT_NOT_CONFIGURED.
See the full Errors catalogue for every code.
Endpoint entitlements (slugs)
Each route is guarded by a named slug attached to your API client. Confirm which are enabled for sandbox and production with your integration contact. Slug names follow the route: {method}_money_v1_{resource}.
| Method | Path (under /money/v1) |
Slug |
|---|---|---|
POST |
/charge |
post_money_v1_charge |
GET |
/charge/:reference |
get_money_v1_charge_reference |
GET |
/charge |
get_money_v1_charge_list |
POST |
/payout |
post_money_v1_payout |
GET |
/payout/:reference |
get_money_v1_payout_reference |
GET |
/payout |
get_money_v1_payout_list |
GET |
/transaction-details/:id |
get_money_v1_transaction_details_id |
GET |
/account |
get_money_v1_account |
PIN encryption
The pin field on POST /charge and POST /payout accepts either form — the API auto-detects which one you sent:
- Plain — the PIN as-is (e.g.
"1234"). TLS protects it in transit. - Encrypted (recommended) — the PIN encrypted with the RSA public key from your integration pack, base64-encoded. The scheme is RSA-OAEP with SHA-256:
const crypto = require('crypto');
const pin = crypto.publicEncrypt(
{ key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
Buffer.from('1234'),
).toString('base64');
Encrypting keeps the PIN unreadable by any intermediary between your backend and Gozem’s verification layer. A value that looks encrypted but cannot be decrypted is rejected with 400.
Idempotency
POST /money/v1/charge and POST /money/v1/payout accept an idempotency-key header — a UUID, recommended on every create. Reusing a key with the same body returns the original cached response, so a retry never charges or pays out twice. Reusing a key with a different body returns 409 IDEMPOTENCY_CONFLICT. Generate a fresh key per distinct operation and persist it before sending, so a crash-and-retry reuses the same key.
Rate limiting
Money routes are rate-limited per API client; over-limit requests return 429. The response conventions and retry guidance are shared platform behaviour — see API Basics. Design retries with exponential backoff.