Errors
The API uses conventional HTTP status codes. Error bodies are JSON. Every response carries an x-request-id header; include it when reporting a problem.
Status codes#
| Status | Meaning | Retry? |
|---|---|---|
400 |
Validation failed; the request never reached the decision engine | No. Fix the request |
401 |
Missing or invalid API key | No. Fix credentials |
403 |
Key lacks a scope, is scoped to another merchant, or the workspace is inactive | No. Fix key configuration |
404 |
Unknown resource, for example an unknown decision_id on an outcome |
No |
429 |
Rate limit exceeded | Yes, after backing off |
5xx |
Server error | Your call: treat like a timeout and use your fallback path |
Validation errors#
Requests are validated with strict schemas. The error echoes field paths and messages, never the submitted values:
{
"error": "validation_error",
"issues": [
{
"path": "transaction.amount",
"code": "too_small",
"message": "Number must be greater than 0"
},
{
"path": "transaction.card_number",
"code": "unrecognized_keys",
"message": "Unrecognized key(s) in object: 'card_number'"
}
]
}
Two validation behaviors worth knowing:
- Unknown fields are rejected, not ignored. This is deliberate: it stops card data and PII from ever entering the system. If you see
unrecognized_keys, remove the field rather than renaming it. - Amounts must be positive integers in minor units.
149.00fails;14900passes.
Rate limit errors#
{ "error": "rate_limited", "limit_per_minute": 2000 }
See Rate limits for the windowing model and handling advice.
Auth and permission errors#
Standard NestJS error bodies with a human-readable message:
{ "statusCode": 403, "message": "API key is missing scopes: decision:write", "error": "Forbidden" }
The full list of authentication failures is in Authentication.
Handling errors in the payment path#
The important distinction is answers versus outages:
- An HTTP error is an answer. The API received your request and rejected it. Retrying the same request will fail the same way. Do not retry 4xx responses.
- A timeout or network error is an outage. You do not know what happened. Retry once if your budget allows, then fall back to your merchant-defined safe default. See Production integration.
The Node SDK encodes this: HTTP errors throw PayInferenceAPIError and are never retried; timeouts and network failures throw PayInferenceTimeoutError or PayInferenceNetworkError, are retried for reporting calls, and trigger the local fallback for decide() when one is configured.
import {
PayInferenceAPIError,
PayInferenceTimeoutError,
PayInferenceNetworkError,
PayInferenceValidationError,
} from '@payinference/sdk-node';
try {
const decision = await payinference.decide(params);
} catch (error) {
if (error instanceof PayInferenceValidationError) {
// Bad input; no request was made. error.issues lists field paths.
} else if (error instanceof PayInferenceAPIError) {
// The API answered with error.status and error.body. Do not retry.
} else if (
error instanceof PayInferenceTimeoutError ||
error instanceof PayInferenceNetworkError
) {
// Outage. Only reachable when no fallback is configured on the client.
}
}
Debugging a failed request#
- Read
issues[].pathon validation errors; it names the exact field. - Check the key mode and scopes on 401/403.
- Capture the
x-request-idresponse header and thedecision_idif one was returned. - Look the decision up in the decision log (
GET /v1/decisions/:decisionIdor the dashboard) to see the matched rules, provider evaluations, and reason codes.