Migrate from v1 — full reference
For the 2-minute executive summary, see Migrate from v1 — quick version.
This document exists for devs auditing the migration, ops team, and edge cases. It lists ~75 integrator-facing routes and how each one behaves on v2.
1. Authentication
Before (v1)
TOKEN_URL="https://corpx-{env}.auth.sa-east-1.amazoncognito.com/oauth2/token"
curl -X POST "$TOKEN_URL" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&scope=api/full"
Now (v2)
TOKEN_URL="https://auth.api.corpx.com/oauth2/token"
curl -X POST "$TOKEN_URL" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&scope=api2/read api2/write"
Differences:
- Token URL changes (goes through our CloudFront).
scope:api/full→api2/read api2/write.- JWT TTL: 1h (same as v1).
Idempotency-Key: optional on v2 (auto-generated if absent; we recommend you keep sending yours for retry control).X-Tenant-Id: required on all/v1/*requests (exceptGET /v1/meand health). Must match the account's tenant whenaccountIdis in the path.
2. Deprecated endpoints (still work until 2026-11-21)
4 paths that existed on v1 and weren't kept in the canonical v2 shape have been re-enabled as deprecated aliases. They keep responding normally — they just attach 3 warning headers to the response:
Deprecation: true
Sunset: Sat, 21 Nov 2026 00:00:00 GMT
Link: </v1/accounts/.../new-path>; rel="successor-version"
These headers follow RFC 8594 (Deprecation) and RFC 9745 (Sunset). Clients can detect them automatically to alert the team.
Planned sunset: 2026-11-21. After that date these routes start returning 410 Gone.
| Deprecated route (still works) | Canonical replacement |
|---|---|
GET /v1/accounts/{id}/pix/payments/{paymentId} | GET /v1/accounts/{id}/pix/payments/lookup?identifier=... |
GET /v1/accounts/{id}/payments/{paymentId} (alias without /pix/) | GET /v1/accounts/{id}/pix/payments/lookup?identifier=... |
GET /v1/accounts/{id}/pix/qr-code (without /lookup) | GET /v1/accounts/{id}/pix/qr-code/lookup?identifier=... |
POST /v1/accounts/{id}/pix/out/qrcode (without hyphen) | POST /v1/accounts/{id}/pix/out/qr-code |
These routes no longer appear in the OpenAPI or the Postman collection — they are documented here only to give you time to migrate. New integrations should use the canonical replacements directly.
3. Removed endpoints (7 routes — return 404)
| v1 route (removed) | What to do |
|---|---|
POST /v1/webhooks/replay | Use statement polling in a short window |
GET /v1/integrator/webhooks | Use GET /v1/webhooks |
GET /v1/integrator/webhooks/{id}/deliveries | No replacement — contact support if you depend on it |
POST /v1/integrator/events/replay | No replacement |
POST /v1/integrator/events/replay/batch | No replacement |
POST /v1/accounts/{id}/pix/med/{medId}/send | Use /decide (but MED is at 503 today) |
POST /v1/accounts/{id}/pix/med/{medId}/response | Use /answer (but MED is at 503 today) |
GET /v1/health still exists on v2. A GET /health alias (without /v1) was added for convenience of external health checks — no action needed.
4. Endpoints with behavioral changes
Same path, same method, but different behavior or JSON shape.
4.1 Statement — cache → live (and reduced payer/payee)
GET /v1/accounts/{id}/statement
v1: read from a local cache (DynamoDB) populated by webhooks. Latency ~50ms; enriched items with tariff_ref and translated labels. Rows had full payer.{bankCode,bankIspb,branch,account,pixKey,...} and beneficiary.{...} because they were assembled from webhooks (which carry every banking field of the counterparty).
v2: calls the settlement bank in real time. Latency ~500ms-1.5s; no derived tariff_ref (fees appear as their own line in the statement); new header X-Source: live. Max window per query: 31 days.
What changed in the row shape
v2 standardized the counterparty object to name + document plus the bank info of our side (settlement bank = MT Bank). The remaining banking fields of the counterparty (branch, account, pixKey, etc.) are no longer present in statement rows because the settlement bank's statement endpoint does not expose them.
v1 (legacy, served from webhook cache):
{
"amount": -100.00,
"direction": "OUT",
"endToEndId": "E...",
"payer": { "name": "...", "document": "...", "bankCode": "681", "bankIspb": "50871921", "branch": "...", "account": "...", "pixKey": "..." },
"beneficiary":{ "name": "...", "document": "...", "bankCode": "001", "bankIspb": "00000000", "branch": "...", "account": "...", "pixKey": "..." }
}
v2 (live, served from the settlement bank):
{
"amount": -100.00,
"direction": "OUT",
"endToEndId": "E...",
"payer": { "name": "MY COMPANY LTDA", "document": "12345678000190", "bankCode": "681", "bankIspb": "50871921", "bankName": "MT Instituição de Pagamentos" },
"payee": { "name": "SUPPLIER XYZ", "document": "98765432000110" },
"counterParty": { "name": "SUPPLIER XYZ", "document": "98765432000110" }
}
Notes on the v2 shape:
- The
payer/payeepair is built from the rowdirection:direction=IN:payer= counterparty (who paid us);payee= our account;direction=OUT:payer= our account;payee= counterparty (who we paid).
- The self side (our account) always brings
name(holder),document(CPF/CNPJ) and the settlement bank identifiers (bankCode,bankIspb,bankName). - The counterparty side always brings only
name+document. Optional fields (bankCode,bankIspb,branch,account,pixKey,accountType,bankName) are omitted when empty — v2'spartyToDTOdoes not emit keys with empty strings. counterParty(extra object withname+documentof the counterparty) is kept to make lookup easier without inspectingdirection.- Removed
authorizationCodefield (the settlement bank's API does not expose it on the statement; it was always empty on v1 when served from the live fallback too).
Where to recover the full counterparty bank payload
If your integration needs branch/account/pixKey/bankCode for the counterparty, use one of the alternatives below (all return the enriched payload, since they come from specific PIX endpoints, not from the statement):
- Outbound webhook:
pix.in.completed,pix.out.completed,pix.refund.completed,qrcode.paid— the CorpX envelope includespayer/payeewith all bank fields. - Single payment lookup:
GET /v1/accounts/{id}/pix/payments/lookup?endToEndId=...— returns the enriched transaction object (same as the webhook). - QR Code:
GET /v1/accounts/{id}/pix/qr-code/lookup?identifier=...— for paid QRs, includespayer+paymentwithendToEnd.
If your integration relied on aggressive statement polling, prefer webhooks:
pix.in.receivedfor received PIXpix.out.confirmedfor completed PIX outboleto.paid/boleto.failedqrcode.paid
The same effects apply to the other endpoints that used to read from the same cache:
GET /v1/accounts/{id}/pix/transactionsGET /v1/accounts/{id}/pix/paymentsGET /v1/accounts/{id}/payments(alias)
All now query the settlement bank in real time, with no local cache.
4.2 MED — at 503 during migration
All /v1/accounts/{id}/pix/med/* endpoints return 503 service_temporarily_unavailable. Reactivation planned for v2.x. If you have active MEDs at cutover time, our team will reach out individually.
med.* webhooks are also suspended during this window — no dispute events will be emitted. You don't need to remove the subscription; when the module returns, events resume automatically.
4.3 Boleto — now active
v1 returned 503 on all boleto endpoints (POST .../boleto/preview, POST .../boleto/pay, GET .../boleto/payments/{id}). v2 is active. See Cash Out guide.
4.4 PIX out — sync vs async (not “everything async”)
v2 keeps the same split as v1:
| Route | Mode | HTTP response |
|---|---|---|
POST .../pix/out | sync | Waits on the workflow up to ~25s. If it finishes in time: 200 with final status (APPROVED, FAILED→422, etc.) and endToEndId when available. On timeout: 202 with status: PENDING + warning — then use lookup/webhooks. |
POST .../pix/out/qr-code | sync | Same as /pix/out (~25s). |
POST .../pix/out/bank-account | sync | Same. |
POST .../pix/out/refund | sync | Same. |
POST .../pix/out/async | async | Immediate 202 with {paymentId, workflowId, idempotencyKey, identifier} — final outcome via webhook or GET .../pix/payments/lookup?identifier=.... |
POST .../pix/out/bank-account/async | async | Immediate 202 (same as /async). |
POST .../pix/out/bigpix (+ bank-account/bigpix) | async | Immediate 202 (batch; server-side chunking unchanged). |
What changed under the hood (no forced migration to async): orchestration uses Temporal, but callers of POST .../pix/out without /async stay on the synchronous path. Move to /async only for high throughput or to avoid holding the HTTP connection for ~25s.
Idempotency: retries with the same Idempotency-Key reuse the same paymentId/workflow while still in flight; after FAILED, a new key may start a new attempt (see Cash Out).
4.5 X-CF-Origin-Verify
v2 only accepts requests coming from CloudFront. Hitting execute-api directly returns 403. Always use https://tenant.api.corpx.com/v1.
4.6 Temporarily suspended webhooks — fee.*, med.*, edi.*
During the migration window, 3 event families are not emitted:
| Family | Status | How to still see the data |
|---|---|---|
fee.* (fee.charged, fee.refunded, etc.) | suspended | Fees appear as standalone lines in the statement (GET /v1/accounts/{id}/statement). Look for type=internal-transfer with identifier=fee-{slug}-{operationReferenceId} to CORPX_FEE_ACCOUNT_ID. |
med.* (med.opened, med.answered, etc.) | suspended | The entire MED module is offline (see §4.2). |
edi.* (CNAB/EDI batch file events) | suspended | Files keep being generated, but webhook notification is paused. Use the CNAB batches GET to track. |
What this means for you:
- Subscriptions to these events can remain registered — when they return, they resume automatically.
- If your reconciliation depended on these events, switch temporarily to statement polling (queries to the settlement bank are real-time, no cache lag).
Planned reactivation in phases:
fee.*— next minor after the no-cache refactor stabilizes.med.*— along with MED module reactivation on v2.x.edi.*— TBD; depends on the new batches pipeline.
4.7 Automatic transaction ↔ fee reconciliation — suspended
v1: the transaction object included a fee: { amount, ... } field that paired each PIX/boleto with its processing fee.
v2: that field was temporarily removed. Reconciliation is done client-side by cross-referencing the identifier:
- Main transaction:
identifier = <your identifier>(or auto-generated) - Matching fee:
identifier = fee-{slug}-{operationReferenceId}in the same time window, debited fromCORPX_FEE_ACCOUNT_ID
How to reconcile via statement:
GET /v1/accounts/{accountId}/statement?startDate=2026-05-21&endDate=2026-05-21
Filter items where identifier starts with fee- and cross-reference with the main transaction via the embedded operationReferenceId.
When the no-cache refactor stabilizes, the fee field returns on the transaction object. No payload change planned for that return — just the field reappearing.
4.8 Internal transfer by-bank-account — optional extra fields
POST /v1/accounts/{id}/transfers/internal/by-bank-account
v2 accepts two extra optional fields: holderDocument and holderName. The current settlement bank needs these; when the client doesn't send them, the backend does the lookup automatically. Not breaking — clients that keep sending only {branch, accountNumber, value, ...} work normally.
4.9 PIX QR-code — flattened response (breaking JSON)
In the table (§6), these endpoints are marked body-diff, not unchanged or generic changed: the path is the same, but the success JSON changed. Code that uses response.data.payload or reads data.location breaks until you update the parser — including POST .../pix/qr-code/dynamic.
Affects the 5 QR-code endpoints:
POST /v1/accounts/{id}/pix/qr-code/dynamicPOST /v1/accounts/{id}/pix/qr-code/staticGET /v1/accounts/{id}/pix/qr-code/lookup(and the deprecated aliasGET /v1/accounts/{id}/pix/qr-code)DELETE /v1/accounts/{id}/pix/qr-code
v1: Finaya-style envelope:
{
"statusCode": 200,
"title": "...",
"type": "...",
"message": "...",
"data": {
"txid": "ABC123",
"payload": "00020126...",
"location": "https://qr.mtbank.com.br/cob/...",
"...": "..."
}
}
v2: flat canonical shape, no envelope:
{
"txid": "ABC123",
"emv": "00020126...",
"type": "dynamic",
"status": "ACTIVE",
"value": 12.34,
"message": "Charge X",
"identifier": "abc123",
"expiresAt": "2026-12-31T23:59:59Z",
"createdAt": "2026-05-21T20:00:00Z"
}
Field mapping:
| v1 (envelope) | v2 (flat) | Notes |
|---|---|---|
data.txid | txid | unchanged |
data.payload | emv | same BRcode/EMV string |
data.location | (removed) | render the QR from emv on the client |
statusCode, title, type, message (envelope) | (removed) | replaced by canonical fields type, status, value, identifier, expiresAt, createdAt |
HTTP status code: v2 returns 201 Created on creation (POST). GET stays 200.
How to migrate (minimum):
- const emv = response.data.payload;
+ const emv = response.emv;
- const qrUrl = response.data.location;
+ // No native equivalent. Render the EMV with qrcode-svg / qrcode.js,
+ // or host it on your own CDN if you need a public URL.
If removing location is a blocker for you, talk to support — we can evaluate reintroducing the field by generating a CDN URL with the cached EMV.
5. New endpoints
| Endpoint | Purpose |
|---|---|
GET /v1/me | Identity of the authenticated client_id (useful for debug) |
GET /v1/transactions/{id}/timeline | Detailed timeline of a transaction (no accountId in the path) |
GET /v1/accounts/{id}/pix/out/bigpix/{batchId} | Aggregated BigPix batch status |
GET /health | Alias of /v1/health (without the /v1 prefix) |
6. Full table
Status legend:
- unchanged: same path and same success response JSON. Your parser stays the same.
- body-diff: same path; different success response JSON (breaking). E.g. QR-code without
{ data: {...} }envelope — see §4.9. - changed: same path; behavior differs (live, latency, 503→200). Success JSON is usually compatible; read the note.
- deprecated: still works until 2026-11-21, with
Deprecation: trueheader. Migrate to the canonical replacement before sunset. - removed: route existed only on v1; returns 404 on v2.
- added: new route; only exists on v2.
| Endpoint | v2 status | Note |
|---|---|---|
GET /v1/health | unchanged | Also accepts GET /health |
GET /v1/accreditations/pf | unchanged | — |
POST /v1/accreditations/pf | unchanged | Body with simplified tier for CNPJ may require Agreement |
PUT /v1/accreditations/pf | unchanged | — |
GET /v1/accreditations/pj | unchanged | — |
POST /v1/accreditations/pj | unchanged | Same |
PUT /v1/accreditations/pj | unchanged | — |
GET /v1/accounts/{id}/balance | unchanged | — |
GET /v1/accounts/{id}/statement | changed | Live (no cache); see §4.1 |
GET /v1/accounts/{id}/transactions/timeline | unchanged | — |
GET /v1/transactions/{id}/timeline | added | New |
POST /v1/accounts/{id}/locked-balance/lock | removed (2026-05) | v2 no longer has local hold. /balance.locked reflects only MT blocked. |
POST /v1/accounts/{id}/locked-balance/unlock | removed (2026-05) | idem |
GET /v1/accounts/{id}/pix/limits | unchanged | — |
PATCH /v1/accounts/{id}/pix/limits | unchanged | — |
POST /v1/accounts/{id}/pix/out | unchanged | Sync (~25s): 200/422 when done; 202+warning on timeout. §4.4 |
POST /v1/accounts/{id}/pix/out/async | unchanged | Async: immediate 202. §4.4 |
POST /v1/accounts/{id}/pix/out/bigpix | unchanged | Single body; server-side chunking; immediate 202 (async). §4.4 |
GET /v1/accounts/{id}/pix/out/bigpix/{batchId} | added | Aggregated batch status |
POST /v1/accounts/{id}/pix/out/bank-account | unchanged | Sync (~25s). §4.4 |
POST /v1/accounts/{id}/pix/out/bank-account/async | unchanged | Async: immediate 202. §4.4 |
POST /v1/accounts/{id}/pix/out/bank-account/bigpix | unchanged | Async: immediate 202 (batch). §4.4 |
POST /v1/accounts/{id}/pix/out/qr-code | unchanged | Sync (~25s), same as /pix/out. §4.4 |
POST /v1/accounts/{id}/pix/out/qr-code/decode | unchanged | — |
POST /v1/accounts/{id}/pix/out/qrcode (alias, no hyphen) | deprecated | Sunset 2026-11-21. Use /pix/out/qr-code |
POST /v1/accounts/{id}/pix/out/refund | unchanged | Sync (~25s). §4.4 |
GET /v1/accounts/{id}/pix/transactions | changed | Live |
GET /v1/accounts/{id}/pix/payments | changed | Live |
GET /v1/accounts/{id}/pix/payments/{paymentId} | deprecated | Sunset 2026-11-21. Use /pix/payments/lookup?identifier= |
GET /v1/accounts/{id}/pix/payments/lookup | unchanged | — |
GET /v1/accounts/{id}/payments (alias) | changed | Live |
GET /v1/accounts/{id}/payments/{paymentId} (alias) | deprecated | Sunset 2026-11-21. Use /pix/payments/lookup?identifier= |
POST /v1/accounts/{id}/boleto/preview | changed | 503 → 200 (boleto active on v2) |
POST /v1/accounts/{id}/boleto/pay | changed | 503 → 200 |
GET /v1/accounts/{id}/boleto/payments/{paymentId} | changed | 503 → 200 |
POST /v1/accounts/{id}/pix/qr-code/static | body-diff | Breaking: no envelope; data.payload→emv; no data.location. §4.9 |
POST /v1/accounts/{id}/pix/qr-code/dynamic | body-diff | Breaking: no envelope; data.payload→emv; no data.location. §4.9 |
GET /v1/accounts/{id}/pix/qr-code (without /lookup) | deprecated | Sunset 2026-11-21. Use /pix/qr-code/lookup?identifier=. Response also body-diff (§4.9) |
GET /v1/accounts/{id}/pix/qr-code/lookup | body-diff | Breaking: same flat shape as POST. §4.9 |
DELETE /v1/accounts/{id}/pix/qr-code | body-diff | Breaking: flat response. §4.9 |
GET /v1/accounts/{id}/pix/qr-codes | unchanged | Paginated list — not the same contract as POST dynamic (§4.9) |
GET /v1/accounts/{id}/pix/qr-codes/stats | unchanged | — |
GET /v1/accounts/{id}/pix/keys | unchanged | — |
POST /v1/accounts/{id}/pix/keys | unchanged | — |
DELETE /v1/accounts/{id}/pix/keys/{pixKey} | unchanged | — |
GET /v1/accounts/{id}/pix/key/{pixKey} | unchanged | DICT lookup |
GET /v1/accounts/{id}/pix/med | changed | 503 stub; see §4.2 |
POST /v1/accounts/{id}/pix/med/{medId}/answer | changed | 503 stub |
POST /v1/accounts/{id}/pix/med/{medId}/decide | changed | 503 stub |
POST /v1/accounts/{id}/pix/med/{medId}/evidence/upload-url | changed | 503 stub |
POST /v1/accounts/{id}/pix/med/{medId}/evidence/add | changed | 503 stub |
POST /v1/accounts/{id}/pix/med/{medId}/evidence/download | changed | 503 stub |
POST /v1/accounts/{id}/pix/med/{medId}/send | removed | Use /decide (at 503 today) |
POST /v1/accounts/{id}/pix/med/{id}/response | removed | Use /answer (at 503 today) |
POST /v1/accounts/{id}/transfers/internal | unchanged | — |
POST /v1/accounts/{id}/transfers/internal/by-bank-account | unchanged | Accepts optional extras (holderDocument, holderName); see §4.8 |
POST /v1/accounts/{id}/transfers/internal/by-document | unchanged | — |
GET /v1/accounts/{id}/transfers/internal/lookup/{document} | unchanged | — |
POST /v1/accounts/{id}/exports | unchanged | — |
GET /v1/accounts/{id}/exports | unchanged | — |
GET /v1/accounts/{id}/exports/{exportId}/download | unchanged | — |
GET /v1/me | added | client_id debug |
GET /v1/webhooks | unchanged | — |
POST /v1/webhooks | unchanged | — |
PUT /v1/webhooks/{subscriptionId} | unchanged | — |
DELETE /v1/webhooks/{subscriptionId} | unchanged | — |
GET /v1/webhooks/events | unchanged | Catalog |
POST /v1/webhooks/replay | removed | Removed |
GET /v1/integrator/webhooks | removed | Use GET /v1/webhooks |
GET /v1/integrator/webhooks/{id}/deliveries | removed | No replacement |
POST /v1/integrator/events/replay | removed | No replacement |
POST /v1/integrator/events/replay/batch | removed | No replacement |
GET /v1/security/ip-allowlist | unchanged | — |
PUT /v1/security/ip-allowlist | unchanged | — |
GET /v1/security/ip-allowlist/audit | unchanged | — |
Summary: 46 unchanged, 5 body-diff (PIX QR-code — flat JSON, §4.9), 12 changed (live statement, MED stubs, boleto 503→200, etc.), 4 deprecated (aliases, sunset 2026-11-21), 7 removed (404 on v2), 3 added.
7. Account IDs preserved
The account_id you use in all paths and webhooks are the same before and after the cutover. You do not need to update references stored in your system. They remain UUIDs.
What changes internally (transparent to you):
- Settlement-bank identifiers are new — you never saw this field.
- Settlement-bank authentication mechanism changed — internal handler.
8. Full FAQ
Does my webhook URL need to change? No.
Does my HMAC secret change? No.
Do account IDs change? No.
Can I go back to v1 if something goes wrong? During the first 4h after cutover, yes — we have a rehearsed rollback. After that, the old infrastructure is gradually decommissioned.
For how long is the old backoffice available? 30 days after cutover.
Does high-frequency statement polling still work? It does, but with higher latency (~1s vs ~50ms of the old cache) and a 31-day max window per query. For changes, prefer webhooks.
Are there new rate limits?
We keep the same per-tenant limits as v1. If you hit them, you get 429 Too Many Requests with Retry-After.
Will the fee field return on the transaction object?
Yes, in the next minor after stabilization. No payload change — just the field reappearing.
Will the fee.* / med.* / edi.* webhooks return?
Yes, in phases: fee.* on the next minor; med.* along with MED module reactivation on v2.x; edi.* TBD.
Support
Contact: suporte-api@corpx.com — response within 1h on business hours.