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.
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.
Full example
This known vector uses the request-signing secret hs_test:
hs_testPOST /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
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
| Status | Common reason |
|---|---|
| 401 | Authorization or API key is invalid |
| 401 | SIGNATURE_MISSING or SIGNATURE_INVALID when HMAC is enabled for the key |
| 401 | SIGNATURE_TIMESTAMP_INVALID when X-Signature-Timestamp is missing, malformed, or outside ±300 seconds |
| 403 | API key is valid but not authorized for this resource |