Error handling
All errors are returned in Problem JSON format (RFC 7807) with Content-Type: application/problem+json.
Structure
{
"type": "https://api-docs.maxpay.com.ar/errors/receivable-account-already-exists",
"title": "Receivable account already exists",
"status": 409,
"detail": "A collection account already exists for this client with the same currency.",
"instance": "/v1/receivable-accounts",
"code": "receivable-account-already-exists",
"requestId": "req_b3f8a1c2d4e5f6789012345678901234",
"existingResourceId": 1042
}
| Field | Type | Always present | Description |
|---|---|---|---|
type | URI | Yes | URI identifying the problem type. Points to the error catalog. |
title | string | Yes | Short summary in English. Stable across problem versions. |
status | integer | Yes | Corresponding HTTP code. |
detail | string | Yes | Human-readable description of the concrete problem. May vary between occurrences. |
instance | relative URI | Yes | Path of the request that generated the error. |
code | string | Yes | Stable identifier of the error (kebab-case). Suitable for ERP mapping. |
requestId | string | Yes | Unique request ID. Include in support reports. |
invalidParams | array | Only on 400 | Detail of invalid fields. |
| Other fields | variable | No | Additional problem-specific context. |
Validation errors (400)
When the problem is request format (missing fields, incorrect types, out-of-range values), the response includes invalidParams:
{
"type": "https://api-docs.maxpay.com.ar/errors/validation",
"title": "Validation error",
"status": 400,
"detail": "The request has invalid fields.",
"instance": "/v1/receivables",
"code": "validation",
"requestId": "req_...",
"invalidParams": [
{ "name": "amount", "reason": "Amount must be positive" },
{ "name": "currencyCode", "reason": "Currency code is required" }
]
}
HTTP categories
| HTTP | Category | Examples |
|---|---|---|
400 | Invalid request format | Bad schema, wrong types, missing fields |
401 | Authentication failed | Invalid, expired, or missing token |
403 | Authorized but no permission | Missing scope, resource belongs to another customer |
404 | Resource doesn't exist | Nonexistent or inaccessible ID |
409 | State conflict | Resource already exists, idempotency-key reused with different body |
422 | Business rule | Account not enabled for debt, invalid state transition |
429 | Rate limit exceeded | Too many requests per minute |
500 | Internal error | Bug in Max Pay |
502/504 | External provider failure | Banking provider unavailable, COELSA timeout |
Full catalog
See Error catalog for the exhaustive list of codes, messages and context.
Recommended handling in the ERP
async function callMaxpay(path, options) {
const res = await fetch(`${BASE_URL}${path}`, options);
if (res.ok) return await res.json();
const problem = await res.json();
const requestId = problem.requestId;
switch (problem.code) {
case 'idempotency-key-reuse':
// Different body with the same key — integrator bug, alert
throw new IntegrationError(problem, requestId);
case 'receivable-account-already-exists':
// Resource already created — use the existing ID
return await callMaxpay(`/v1/receivable-accounts/${problem.existingResourceId}`, {
method: 'GET',
headers: options.headers
});
case 'rate-limit-exceeded':
const retryAfter = parseInt(res.headers.get('Retry-After'), 10);
await sleep(retryAfter * 1000);
return await callMaxpay(path, options);
case 'external-provider-unavailable':
case 'external-provider-timeout':
// Retry with exponential backoff
throw new RetryableError(problem, requestId);
default:
throw new MaxpayError(problem, requestId);
}
}
Reporting an incident
When reporting an error to support, always include the requestId and the code. This allows finding the full trace of the request in our logs.