Errors from the ConnectPSP API return a standardized RFC 7807 Problem Details JSON object. Two authentication failures are the exception — see Authentication Errors.
{
"type": "https://doc.connectpsp.com/errors/duplicated-external-reference",
"title": "Referência Externa Duplicada",
"status": 409,
"code": "DUPLICATED_EXTERNAL_REFERENCE",
"detail": "Já existe uma operação com o externalReference informado: withdraw_xyz789."
}| Field | Description |
|---|---|
type | URI that identifies the error category. |
title | Short, human-readable summary (stable across occurrences). |
status | HTTP status code. |
code | Machine-readable error code. Use this for programmatic handling. |
detail | Human-readable explanation of this specific error occurrence. |
Two different things can go wrong with a Cash-Out, and they reach you through different channels:
| What | How you find out |
|---|---|
The request itself is invalid (bad body, duplicated externalReference, reused idempotency key) | An HTTP error on POST /cash-out, described on this page |
The request was accepted (202) but could not be paid — balance, limits, PIX key, receiving institution | A CASHOUT_FAILED webhook with status: "CANCELLED" and a failure.code |
POST /cash-out does not check your balance or your withdrawal limits before answering. Those checks run after the 202 Accepted, so an insufficient balance shows up as failure.code: "INSUFFICIENT_FUNDS" on a cancelled Cash-Out, never as an HTTP error. The full list of failure.code values, with their messages and whether to retry, is in the Webhooks guide.
| Status | When it occurs |
|---|---|
400 Bad Request | Invalid JSON, missing required field, field validation failed, or invalid X-Idempotency-Key. |
401 Unauthorized | Wrong credentials on POST /auth/token; missing, expired or invalid JWT; missing or invalid ApplicationToken; missing or invalid DigitalSignature. |
403 Forbidden | No CryptoToken configured for your client, so DigitalSignature cannot be checked. |
404 Not Found | The requested transaction does not exist for this client. |
409 Conflict | Duplicated externalReference on a Cash-In or Cash-Out, or an X-Idempotency-Key still being processed. |
422 Unprocessable Entity | Business rule violation (see codes below). |
500 Internal Server Error | Unexpected server error — retry with backoff, contact support if it persists. |
These errors have no code. Tell them apart by the endpoint, the body and the headers.
| Case | Status | What you receive | What to do |
|---|---|---|---|
Wrong clientId or clientSecret on POST /auth/token (also a clientSecret that is not a GUID) | 401 | { "message": "As credenciais informadas não são válidas." } — a plain object, not Problem Details | Check both credentials. The message is the same whichever one is wrong. |
clientId or clientSecret missing on POST /auth/token | 400 | Problem Details with code: "VALIDATION_ERROR" | Send both fields. |
| JWT missing, invalid or expired, on any other endpoint | 401 | No body. The reason is in the WWW-Authenticate header (below) | Request a new token with POST /auth/token and retry. |
ApplicationToken header missing | 401 | Problem Details, title: "ApplicationToken Ausente" | Send the header. |
ApplicationToken not a GUID, or not found / inactive | 401 | Problem Details, title: "ApplicationToken Inválido"; detail says which | Check the value issued by ConnectPSP. |
The JWT is checked first: a request with an expired JWT and a wrong ApplicationToken gets the JWT 401. The WWW-Authenticate header of the JWT 401:
| Situation | WWW-Authenticate |
|---|---|
No Authorization: Bearer header | Bearer |
| Token expired | Bearer error="invalid_token", error_description="The token expired at '<date>'" |
| Token rejected for another reason (bad signature, malformed) | Bearer error="invalid_token", error_description="<reason>" |
The ApplicationToken 401 body:
{
"type": "https://doc.connectpsp.com/errors/unauthorized",
"title": "ApplicationToken Inválido",
"status": 401,
"detail": "O ApplicationToken fornecido não foi encontrado ou está inativo."
}Only on POST /cash-out and POST /cash-in/{id}/refund. The DigitalSignature is checked after the JWT and the ApplicationToken, and after the body is validated (an invalid body answers 400 VALIDATION_ERROR first). These errors carry a code.
| Code | Status | Description |
|---|---|---|
DIGITAL_SIGNATURE_REQUIRED | 401 | The DigitalSignature header is missing. |
DIGITAL_SIGNATURE_INVALID | 401 | The HMAC does not match your JWT and CryptoToken. See the Authentication Guide. |
CRYPTO_TOKEN_NOT_CONFIGURED | 403 | Your client has no active CryptoToken, so the signature cannot be checked. Ask ConnectPSP support to issue one. |
Common to the Cash-In and Cash-Out endpoints.
| Code | Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | One or more request fields failed validation, or the JSON body is invalid. errors lists the messages per field. |
DUPLICATED_EXTERNAL_REFERENCE | 409 | You already created a Cash-In (on POST /cash-in) or a Cash-Out (on POST /cash-out) with this externalReference — even one that was later cancelled — or one with it is being created right now. |
INTERNAL_ERROR | 500 | Unexpected error on our side. Retry with backoff; if it persists, send the traceId to support. |
Only when you send the optional X-Idempotency-Key header on POST /cash-in or POST /cash-out. See Idempotency.
| Code | Status | Description |
|---|---|---|
IDEMPOTENCY_KEY_INVALID | 400 | The key is longer than 100 characters. |
IDEMPOTENCY_KEY_IN_PROGRESS | 409 | A request with this key is still being processed. Check the result in a moment. |
IDEMPOTENCY_KEY_REUSED | 422 | The key was already used with a different body. Generate a new key for a new operation. |
| Code | Status | Description |
|---|---|---|
CASHIN_NOT_FOUND | 404 | No Cash-In matches the {id} for this client (GET /cash-in/{id} and POST /cash-in/{id}/refund). |
REFUND_NOT_ALLOWED | 422 | The Cash-In status is not PAID — not paid yet, expired, cancelled, or already refunded. detail says which. |
MISSING_E2E | 422 | The payment endToEndId is not available yet, so the devolution cannot be sent. Retry later. |
| Code | Status | Description |
|---|---|---|
CASHOUT_NOT_FOUND | 404 | No Cash-Out matches the {id} for this client (GET /cash-out/{id}). |
RULE_VIOLATION | 422 | Generic business rule violation from the payment flow. Check detail. |
Why an accepted Cash-Out was cancelled is not an HTTP error: it comes as failure.code on the CASHOUT_FAILED webhook and on GET /cash-out/{id}. See the failure.code reference.
| Code | Status | Description |
|---|---|---|
INSUFFICIENT_PROPRIETARY_FUNDS | 422 | Top-up failed — not enough funds in the Proprietary account. |
INSUFFICIENT_TRANSACTIONAL_FUNDS | 422 | Sweep failed — not enough funds in the Transactional account. |
RETROACTIVE_BALANCE_NOT_IMPLEMENTED | 422 | Historical balance lookup is not yet available. |
INVALID_AMOUNT | 422 | Rebalance amount must be greater than zero. |
Read-only endpoints, so no business rules — only "no data" and "try again". Note that the code here is kebab-case for the invalid-parameter cases, unlike every other code in this page.
| Code | Status | Description |
|---|---|---|
PIX_HEALTH_UNAVAILABLE | 503 | We could not read the picture or the incidents. Reflects an internal read failure, not the state of PIX. Honour Retry-After (60 s) and retry with backoff. |
PIX_INCIDENT_NOT_FOUND | 404 | No incident with that id is visible to you — it does not exist, or it is outside your scope. The two cases are indistinguishable on purpose. |
invalid-window | 422 | from is later than to, or the window exceeds 92 days. |
invalid-status | 422 | A status value outside ACTIVE, WORSENING, RECOVERING, RESOLVED. |
invalid-page | 422 | page below 1. |
invalid-page-size | 422 | pageSize outside 1–200. |
invalid-incident-id | 422 | The id in the path is blank or only whitespace. |
GET /health/pix also answers 204 No Content when the engine has not produced any picture yet (a freshly deployed environment, for example). That is not an error — treat it as "no data yet" and retry later.
async function createCashOut(payload, idempotencyKey) {
const response = await fetch('https://api.homologacao.connectpsp.com/cash-out', {
method: 'POST',
headers: {
/* Authorization, ApplicationToken, DigitalSignature, Content-Type ... */
'X-Idempotency-Key': idempotencyKey, // optional, recommended: reuse it only to retry this same request
},
body: JSON.stringify(payload)
});
if (response.status === 401 && response.headers.get('WWW-Authenticate')) {
// JWT missing, invalid or expired: no body. Get a new token and retry.
await refreshToken();
return createCashOut(payload, idempotencyKey);
}
if (!response.ok) {
const error = await response.json();
switch (error.code) {
case 'DUPLICATED_EXTERNAL_REFERENCE':
// A Cash-Out with this externalReference already exists: GET /cash-out/{id}
// accepts the externalReference, so fetch it and follow its status.
return getCashOut(payload.externalReference);
case 'IDEMPOTENCY_KEY_IN_PROGRESS':
// The first attempt with this key is still running. Wait and check it.
await sleep(2000);
return getCashOut(payload.externalReference);
case 'IDEMPOTENCY_KEY_REUSED':
// Bug on our side: the same key was used for a different body.
throw new Error('X-Idempotency-Key reused with a different payload.');
case 'VALIDATION_ERROR':
throw new Error(`Invalid Cash-Out request: ${error.detail}`);
default:
// Log and escalate unknown errors
logger.error('ConnectPSP error', error);
throw new Error(error.detail);
}
}
// 202 Accepted: queued, NOT paid. The outcome arrives by webhook.
return response.json();
}function onCashOutEvent(event) {
const { status, failure, reason } = event.data;
switch (event.eventType) {
case 'CASHOUT_COMPLETED':
// status === 'COMPLETED'
return markPaid(event.data);
case 'CASHOUT_FAILED':
// status === 'CANCELLED': the money never left your account
switch (failure.code) {
case 'INSUFFICIENT_FUNDS':
return triggerRebalanceAlert(event.data);
case 'PIX_KEY_NOT_FOUND':
case 'PIX_KEY_INVALID':
return askUserForAnotherKey(event.data);
default:
return markCancelled(event.data, failure.code, failure.message);
}
case 'CASHOUT_REFUNDED':
// status === 'REFUNDED': paid, then returned to your account
return markRefunded(event.data, reason);
}
}