Gozem Developer DocsDocs

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:

  • METHOD is upper case (GET, POST).
  • path is 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 what JSON.stringify('') produces, and it is the most common cause of 401 Invalid request signature on GET calls.
  • The body hash uses an empty-string key; only the request signature uses your HMAC secret.
  • Clock skew: requests are rejected if x-timestamp differs 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:

  1. Endpoint slug — your client must be entitled to the route’s slug (see below), or 403 SEC_ENDPOINT_NOT_AUTHORIZED.
  2. Scope — your client must hold the OAuth scope the operation requires.
  3. HMAC — signature and timestamp must validate, or 401.
  4. Merchant linkage — your client must resolve to a merchant account with a primary wallet, or 403 MERCHANT_NOT_RESOLVED / 400 MERCHANT_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.