Core Concepts
Errors
Every error response uses the same envelope. Switch on error.code, not error.message — message wording isn't guaranteed stable across releases.
{
"error": {
"type": "invalid_request",
"code": "unsafe_webhook_url",
"message": "Webhook URL resolves to a private or reserved address.",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}request_id is a bare UUIDv4, matching the X-Request-Id response header on every response, success or error.
HTTP status → error type
| HTTP | type |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | conflict |
| 422 | validation_error |
| 429 | rate_limited |
| 500 | internal_error |
| 503 | service_unavailable |
Codes worth handling explicitly
400 invalid_request
- unsafe_webhook_url — Webhook URL resolves to a private/internal address.
- invalid_cursor — Malformed pagination cursor.
401 authentication_failed
- timestamp_expired — Timestamp outside the 5-minute window.
- signature_invalid — Signature doesn't match the raw request body.
- replay_detected — Nonce was already used within the window.
403 permission_denied
- key_inactive — The API key is known but not active.
409 conflict
- idempotency_key_conflict — Same Idempotency-Key reused with a different request body.
- request_in_progress — Same key + body, previous attempt hasn't reached a definitive outcome yet.
503 service_unavailable
- bank_timeout — The bank connector didn't respond in time. The payment's true outcome is unknown and it is not marked failed — retry the same Idempotency-Key later.
Other 5xx responses: retry with exponential backoff — this is a Prosoft Pay-side incident, not a request problem. 429 rate_limited does not currently return a Retry-After header, so use your own backoff (a 60-second sliding window per API key).