# Migration guide: v2.1 → v2.2 Read this page if you **already integrate with the API v2** and your code handles Cash-Outs, Cash-Ins, webhooks or error responses. It lists what changed in v2.2 and what to change in your code. The full list of changes is in the [Changelog](/changelog). **What does not change:** - **URLs** — the same base URLs and the same endpoints. - **Authentication** — the same `POST /auth/token`, `Authorization`, `ApplicationToken` and `DigitalSignature`. - **Webhook event names** — `CASHIN_*` and `CASHOUT_*` keep their names, including `CASHIN_REQUESTED`, `CASHOUT_REQUESTED` and `CASHOUT_FAILED`. What changes is the `data` inside some of them. ## Breaking changes In the tables below, **Before** is what v2.1 returned or documented, and **After** is the v2.2 contract. ### Cash-Out | Field | Before (v2.1) | After (v2.2) | | --- | --- | --- | | `status` — paid | `PAID` (and `COMPLETED`, documented as a legacy synonym) | `COMPLETED` | | `status` — money never left | `FAILED` | `CANCELLED` | | `status` — held on the ConnectPSP side | `ANALYSING` could appear | `PENDING` | | `status` — full list | `PROCESSING`, `PENDING`, `PAID`, `FAILED`, `REFUNDED`, `CANCELLED`, `COMPLETED` | `PROCESSING`, `PENDING`, `COMPLETED`, `CANCELLED`, `REFUNDED` | | Payment date | `paidAt` | `completedAt` | | Failure date | `failedAt` | Removed. A cancelled Cash-Out carries `cancelledAt` | | Cancellation date | `canceledAt` (one L) | `cancelledAt` (two L, like the `CANCELLED` status) | | `failure` | On `FAILED` | **Only** on `CANCELLED` | | `failure.code` | `REJECTED` or `GENERIC_ERROR` | One code from a fixed catalog of 18 codes — see the [`failure.code` table](/guides/webhooks#failure-codes) | | `failure.message` | Free text, in Portuguese | Fixed per `code`, in English | | `reason` | Free text | A code, **only** on `REFUNDED`: `PAYEE_REQUESTED`, `FRAUD`, `OPERATIONAL_ERROR`, `REVERSED_BY_INSTITUTION` or `OTHER` | | `payee.destination.pixKeyType` | `"CPF/CNPJ"` for a document key | `CPF` or `CNPJ`. The full list is `CPF`, `CNPJ`, `EMAIL`, `PHONE`, `EVP` | | Insufficient balance on `POST /cash-out` | `422` with `code: "INSUFFICIENT_FUNDS"` | `202 Accepted`, then `CASHOUT_FAILED` with `failure.code: "INSUFFICIENT_FUNDS"`. Never a `422` | | `externalReference` maximum length | 100 characters (documented) | 50 characters | The rule behind the statuses: if the money never left your account, it is `CANCELLED`; if it left and came back, it is `REFUNDED`. ### Cash-In | Field | Before (v2.1) | After (v2.2) | | --- | --- | --- | | Cancellation date | `canceledAt` (one L) | `cancelledAt` (two L). `CANCELLED` is now documented on `GET /cash-in/{id}` | | `reason` | Not returned | A code, **only** on `REFUNDED`: `FRAUD`, `DUPLICATED_PAYMENT`, `CUSTOMER_REQUEST`, `MERCHANT_ERROR`, `PAYER_NOT_ALLOWED` or `OTHER` | | `externalReference` maximum length | 100 characters (documented) | 50 characters | ### Webhooks | Event | Before (v2.1) | After (v2.2) | | --- | --- | --- | | `CASHIN_REQUESTED` | `data.status: "REQUESTED"` | `data.status: "AWAITING_PAYMENT"`, as on `POST /cash-in` and `GET /cash-in/{id}` | | `CASHOUT_REQUESTED` | `data.status: "REQUESTED"` | `data.status: "PROCESSING"`, as on `POST /cash-out` and `GET /cash-out/{id}` | | `CASHOUT_COMPLETED` | `data.status: "COMPLETED"`, `completedAt` | No change in these fields — `GET /cash-out/{id}` now uses them too. Adds `settlement` | | `CASHOUT_FAILED` | `data.status: "FAILED"`, `failure.code` `REJECTED` / `GENERIC_ERROR`, `failedAt` | Same event name. `data.status: "CANCELLED"`, `failure` from the catalog, `cancelledAt` | | `CASHOUT_REFUNDED` | `reason` as free text | `reason` as a code. Adds `settlement` | | `CASHIN_REFUNDED` | No `reason` | `reason` as a code | Every Cash-Out field change in the table above applies to the webhooks too: webhook `data` is the same object as `GET /cash-out/{id}`. ### Errors | Case | Before (v2.1 documentation) | After (v2.2) | | --- | --- | --- | | Wrong credentials on `POST /auth/token` | `401 INVALID_CREDENTIALS` | `401` with `{ "message": "..." }`, a plain object with **no `code`** | | JWT expired | `401 TOKEN_EXPIRED` | `401` with **no body** and a `WWW-Authenticate` header (`error_description="The token expired at '...'"`). Request a new token | | JWT missing or invalid | `401` | `401` with **no body** and a `WWW-Authenticate` header | | `ApplicationToken` missing or invalid | `401 INVALID_APPLICATION_TOKEN` | `401` Problem Details with **no `code`**; tell the cases apart by `title` | | `DigitalSignature` missing or wrong | `422 INVALID_SIGNATURE` | `401 DIGITAL_SIGNATURE_REQUIRED` or `401 DIGITAL_SIGNATURE_INVALID`; `403 CRYPTO_TOKEN_NOT_CONFIGURED` when your client has no CryptoToken | | Insufficient balance on `POST /cash-out` | `422 INSUFFICIENT_FUNDS` | Not an HTTP error: `CASHOUT_FAILED` with `failure.code: "INSUFFICIENT_FUNDS"` | | PIX key not found on `POST /cash-out` | `422 PIX_KEY_NOT_FOUND` | Not an HTTP error: `CASHOUT_FAILED` with `failure.code: "PIX_KEY_NOT_FOUND"` | | Transaction not found | `404 NOT_FOUND` | `404 CASHIN_NOT_FOUND` or `404 CASHOUT_NOT_FOUND` | | Refund of a Cash-In that cannot be refunded | `422 CASH_IN_NOT_PAID`, `CASH_IN_ALREADY_REFUNDED`, `CASH_IN_EXPIRED`, `CASH_IN_CANCELLED` | `422 REFUND_NOT_ALLOWED`; `detail` says which case | | Refund before the payment E2E is available | `422 MISSING_END_TO_END_ID` | `422 MISSING_E2E` | The authentication errors are described in [Authentication Errors](/guides/error-handling#authentication-errors). ### Summary `GET /cash-out/summary`: | Bucket | Before (v2.1) | After (v2.2) | | --- | --- | --- | | Paid | `totals.paid` | `totals.completed` | | Failed | `totals.failed` | Removed. Counted under `totals.cancelled` | | Full list of keys | `processing`, `pending`, `paid`, `failed`, `refunded`, `cancelled` | `processing`, `pending`, `completed`, `cancelled`, `refunded` — the Cash-Out statuses in lowercase | | `settlement` | Paid transactions | `completed` only | `GET /cash-in/summary` does not change: the Cash-In paid bucket is still `paid`. The reconciliation formula becomes `cashOut.totals.completed.amount` where it used `cashOut.totals.paid.amount` — see [Reconciliation](/guides/reconciliation). ## Behavior changes These do not break a correct integration, but you may see different values than before. - **`expiredAt` is the charge's expiration date and time** (the end of the payment window), with the same value on `GET /cash-in/{id}`, `GET /cash-in` and `CASHIN_EXPIRED`. In v2.1 the value could differ between channels. The envelope `eventAt` is the send time, not the expiration. - **Cash-In webhook `data` follows the `GET` rules**, `amount` included: the `data` of every Cash-In event (except `CASHIN_REQUESTED`) is the same object `GET /cash-in/{id}` returns at that moment. The one documented difference is on refunded Cash-Ins — see [Event Structure](/guides/webhooks#event-structure). - **`settlement` appears only when a bank is involved.** Cash-In: on `PAID` and `REFUNDED`, now also in the `data` of `CASHIN_PAID` and `CASHIN_REFUNDED`. Cash-Out: on `COMPLETED` and `REFUNDED`, and on `CANCELLED` when a bank had already been assigned (no money left your account). Never on `PROCESSING` or `PENDING`. - **`X-Idempotency-Key` is optional** (still recommended) on `POST /cash-in` and `POST /cash-out`. Without it the request is processed normally, and `externalReference` still blocks duplicates. - **`409 DUPLICATED_EXTERNAL_REFERENCE` also on `POST /cash-in`**, as on `POST /cash-out`. ## Additions - **`reason`** on refunded Cash-Outs and Cash-Ins, as a code. - **`settlement`** in the `data` of `CASHIN_PAID` and `CASHIN_REFUNDED`. - **A `code` on every error** except the authentication `401`s: `VALIDATION_ERROR`, `CASHIN_NOT_FOUND`, `CASHOUT_NOT_FOUND`, `REFUND_NOT_ALLOWED`, `MISSING_E2E`, `INTERNAL_ERROR` and the `DigitalSignature` codes. See [Error Handling](/guides/error-handling). - **`GET /cash-in/{id}` and `GET /cash-out/{id}` accept your `externalReference`**, besides `internalCode`, `transactionId` and the payment or refund `endToEndId`. When a value matches more than one transaction, a fixed precedence decides: `internalCode`, then `transactionId`, then payment `endToEndId`, then refund `endToEndId`, then `externalReference`; within the same level, the most recent transaction wins. - **`Idempotent-Replayed: true`** header when a request with the same `X-Idempotency-Key` and the same body is replayed within 24 hours. - **Idempotency error codes**: `400 IDEMPOTENCY_KEY_INVALID`, `409 IDEMPOTENCY_KEY_IN_PROGRESS`, `422 IDEMPOTENCY_KEY_REUSED`. See [Idempotency](/guides/authentication#idempotency). ## Checklist for integrators Search your code for each item and apply the action. | Search for | Where | Action | | --- | --- | --- | | `PAID` | Cash-Out status handling | Replace with `COMPLETED`. Keep `PAID` for Cash-In, where it does not change | | `FAILED` | Cash-Out status handling | Replace with `CANCELLED`. The `CASHOUT_FAILED` event name stays | | `ANALYSING` | Cash-Out status handling | Remove. Treat the stage as `PENDING` | | `paidAt` | Cash-Out | Read `completedAt` | | `failedAt` | Cash-Out | Read `cancelledAt` | | `canceledAt` | Cash-Out and Cash-In | Read `cancelledAt` (two L) | | `REJECTED`, `GENERIC_ERROR` | `failure.code` handling | Map the [18 catalog codes](/guides/webhooks#failure-codes). Treat a code you do not know as `PROCESSING_ERROR` | | Parsing of `failure.message` | Cash-Out | Key your logic on `failure.code`. The message is fixed English text | | Parsing of `reason` | Cash-Out | Read `reason` as a code, only on `REFUNDED` | | `CPF/CNPJ` | `pixKeyType` handling | Accept `CPF` and `CNPJ` as separate values | | `"REQUESTED"` | `CASHIN_REQUESTED` / `CASHOUT_REQUESTED` handlers | Expect `AWAITING_PAYMENT` (Cash-In) and `PROCESSING` (Cash-Out) | | `totals.paid` | `GET /cash-out/summary` | Read `totals.completed` | | `totals.failed` | `GET /cash-out/summary` | Read `totals.cancelled`, where failed Cash-Outs are now counted | | `INSUFFICIENT_FUNDS` on the `POST /cash-out` response | Cash-Out creation | Move the handling to `CASHOUT_FAILED` with `failure.code: "INSUFFICIENT_FUNDS"` | | `PIX_KEY_NOT_FOUND` on the `POST /cash-out` response | Cash-Out creation | Move the handling to `CASHOUT_FAILED` with `failure.code: "PIX_KEY_NOT_FOUND"` | | `INVALID_CREDENTIALS`, `TOKEN_EXPIRED`, `INVALID_APPLICATION_TOKEN` | Authentication handling | Handle by status and endpoint: a `401` with a `WWW-Authenticate` header means a new JWT is needed. See [Authentication Errors](/guides/error-handling#authentication-errors) | | `INVALID_SIGNATURE` | `DigitalSignature` handling | Handle `401 DIGITAL_SIGNATURE_REQUIRED`, `401 DIGITAL_SIGNATURE_INVALID` and `403 CRYPTO_TOKEN_NOT_CONFIGURED` | | `NOT_FOUND`, `CASH_IN_*`, `MISSING_END_TO_END_ID` | Error handling | Use `CASHIN_NOT_FOUND`, `CASHOUT_NOT_FOUND`, `REFUND_NOT_ALLOWED` and `MISSING_E2E` | | `externalReference` generation | Cash-In and Cash-Out creation | Keep it at 50 characters or fewer | ## Side-by-side example The same cancelled Cash-Out, delivered as `CASHOUT_FAILED`. All data is fictitious. **v2.1** ```json { "eventType": "CASHOUT_FAILED", "eventAt": "2026-03-10T14:00:20Z", "data": { "transactionId": "3f9a1c52-7b64-4d0e-9a1b-2c8e5f7d4a10", "internalCode": "QWERT12345", "externalReference": "withdraw_example_001", "status": "FAILED", "amount": 500.00, "failure": { "code": "REJECTED", "message": "Transferência rejeitada pela instituição recebedora." }, "payee": { "name": "Maria Silva", "document": "12345678909", "destination": { "type": "PIX", "pixKeyType": "CPF/CNPJ", "pixKey": "12345678909" } }, "requestedAt": "2026-03-10T14:00:00-03:00", "failedAt": "2026-03-10T14:00:15-03:00" } } ``` **v2.2** ```json { "eventType": "CASHOUT_FAILED", "eventAt": "2026-03-10T14:00:20Z", "data": { "transactionId": "3f9a1c52-7b64-4d0e-9a1b-2c8e5f7d4a10", "internalCode": "QWERT12345", "externalReference": "withdraw_example_001", "status": "CANCELLED", "amount": 500.00, "failure": { "code": "PAYEE_BANK_REJECTED", "message": "The receiving institution rejected the transfer." }, "payee": { "name": "Maria Silva", "document": "12345678909", "destination": { "type": "PIX", "pixKeyType": "CPF", "pixKey": "12345678909" } }, "settlement": { "ispb": "87654321", "bank": "SETTLEMENT PARTNER B", "branch": "0001", "account": "654987" }, "requestedAt": "2026-03-10T14:00:00-03:00", "cancelledAt": "2026-03-10T14:00:15-03:00" } } ``` What changed: `status` `FAILED` → `CANCELLED`; `failure.code` `REJECTED` → `PAYEE_BANK_REJECTED`, with its fixed English message; `pixKeyType` `CPF/CNPJ` → `CPF`; `failedAt` → `cancelledAt`; and `settlement` is present because a bank had already been assigned. No money left the account in either version.