· API Docs

Authentication

Every /v1 request still requires Authorization: Bearer sk_..., the merchant IP allowlist, and rate-limit admission. HMAC request signing is optional and additive: a key in either the `test` or `live` environment with HMAC signing enabled requires X-Signature-Timestamp and X-Signature. Mandatory Idempotency-Key behavior is unchanged.

ClientG2Wallet APIPrepare the Bearer API key required on every /v1 requestAuthorization: Bearer sk_...opt[for a key in either the `test` or `live` environment with HMAC enabled]Join the raw request bytes in order; do not parse, decode, normalize, or sortMETHOD\nPATH_AND_QUERY\nTIMESTAMP\nIDEMPOTENCY_KEY\nBODYhs_ is revealed once and reused until rotate/disableSend Bearer; add X-Signature-Timestamp + X-Signature only when HMAC is enabled1When HMAC is enabled: require exact-one headers, ±300 seconds, and a valid signature2alt[HMAC disabled, or verification passes when enabled]200 OK3[only when HMAC is enabled: invalid headers, timestamp, or signature]401 Unauthorized4
Bearer is always required; HMAC is an optional additive gate per API key in either the `test` or `live` environment

Payload format for signing

Join 5 parts with a newline character \n in this order:

METHOD\nPATH_AND_QUERY\nTIMESTAMP\nIDEMPOTENCY_KEY\nBODY
  • METHOD — the HTTP method exactly as sent; pass the standard uppercase token, e.g. GET or POST
  • PATH_AND_QUERY — the raw, already-encoded path and query exactly as sent, e.g. /v1/payins?expand=fees; do not decode, normalize, or sort it
  • TIMESTAMP — canonical Unix seconds, exactly matching X-Signature-Timestamp and within ±300 seconds of server time
  • IDEMPOTENCY_KEY — the exact Idempotency-Key header value, or an empty string when the header is absent
  • BODY — the raw request body bytes exactly as sent, or empty bytes when there is no body

For a retry of the same logical operation, reuse the same Idempotency-Key but create a fresh timestamp and signature.

Compute signature

Use HMAC-SHA256 with the hs_... request-signing secret revealed once, not the Bearer API key. Reuse it until you rotate or disable signing, and put the lowercase hex digest in X-Signature.

typescript
import crypto from 'node:crypto';

type SignInput = {
  method: string;
  pathAndQuery: string;       // raw target, e.g. '/v1/payins?expand=fees'
  timestamp: number;          // Unix seconds
  idempotencyKey?: string;    // exact header value, or ''
  rawBody?: Buffer;           // exact bytes sent on the wire, or empty
  secret: string;             // hs_... secret revealed once; reuse until rotate/disable
};

export function sign({
  method,
  pathAndQuery,
  timestamp,
  idempotencyKey = '',
  rawBody = Buffer.alloc(0),
  secret,
}: SignInput) {
  const prefix = [
    method,
    pathAndQuery,
    String(timestamp),
    idempotencyKey,
  ].join('\n') + '\n';
  return crypto.createHmac('sha256', secret)
    .update(prefix, 'utf8')
    .update(rawBody)
    .digest('hex');
}

Full example

This known vector uses the request-signing secret hs_test:

hs_test
POST /v1/payins?expand=fees
Authorization: Bearer sk_live_...
Idempotency-Key: idem_test_123
X-Signature-Timestamp: 1700000000
X-Signature: 53d0ef1c531232448fd78ecb63a0fceeda47f96dae407ce13ce31fc9e43ec2d8
Content-Type: application/json

{}

The signed payload ends with BODY {}:

POST
/v1/payins?expand=fees
1700000000
idem_test_123
{}

Verifying webhook callbacks

Webhook signing is separate from API request signing. It uses each endpoint's independent signing_secret, headers X-P2P-Timestamp and X-P2P-Signature: v1=..., and signs TIMESTAMP.RAW_BODY—not the five-field API request string.

  • X-P2P-Timestamp — Unix seconds for that delivery attempt
  • RAW_BODY — request body bytes exactly as received, captured before parsing JSON
  • signing_secret — the webhook endpoint secret, not an API key or request-signing secret
  • X-P2P-Signature — v1= followed by the lowercase hex HMAC-SHA256 digest

Compute HMAC-SHA256(signing_secret, X-P2P-Timestamp + "." + RAW_BODY), prefix the lowercase hex result with v1=, and compare in constant time.

Verification example

typescript
import crypto from 'node:crypto';

// rawBody must be the request bytes captured before parsing JSON.
// headers: callback request headers (lowercase keys)
export function verifyCallback(
  rawBody: Buffer,
  headers: Record<string, string>,
  signingSecret: string,
): boolean {
  const rawTimestamp = headers['x-p2p-timestamp'] ?? '';
  if (!/^(0|[1-9][0-9]*)$/.test(rawTimestamp)) return false;
  const timestamp = Number(rawTimestamp);
  if (Math.abs(Date.now() / 1000 - timestamp) > 5 * 60) return false; // ±5 minutes

  const payload = Buffer.concat([Buffer.from(rawTimestamp + '.'), rawBody]);
  const digest = crypto.createHmac('sha256', signingSecret).update(payload).digest('hex');
  const expected = 'v1=' + digest;
  const given = headers['x-p2p-signature'] ?? '';
  return given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Webhook delivery & retries

  • Acknowledge with any HTTP 2xx status. A 4xx is a permanent rejection; a 5xx or timeout may be retried. The response body is ignored.
  • Every delivery attempt is re-signed with a fresh X-P2P-Timestamp and X-P2P-Signature.
  • The body is an {id,event,livemode,created_at,data} envelope. De-duplicate automatic retries by the body id, which remains the same across those attempts.
  • Branch on the body event. Merchant staff can manually resend only payin.succeeded, and that manual resend creates a new body id.
  • Deliveries can arrive out of order (a retry of an older status may land after a newer one) — before overwriting state, compare updated_at / finalized_at or confirm with GET /v1/payins/{id} / GET /v1/payouts/{id}.
  • Callback URLs are configured only in the Dashboard ("Webhooks" section) and cannot be set per transaction. Payin and Payout may share one URL; branch on the body event.

Auth-related errors

StatusCommon reason
401Authorization or API key is invalid
401SIGNATURE_MISSING or SIGNATURE_INVALID when HMAC is enabled for the key
401SIGNATURE_TIMESTAMP_INVALID when X-Signature-Timestamp is missing, malformed, or outside ±300 seconds
403API key is valid but not authorized for this resource