# Zypher Global — API > Zypher Global payments API, PIX (BRL), SPEI (MXN) + USDT in a single integration. Cash-in, cash-out, status lookup, splits and webhooks, authenticated by Client ID/Secret. Base URL: https://zypher.lat Docs: https://zypher.lat/en/docs ## About A single integration serves both those who receive/pay with their own account and those operating as a platform/acquirer: cash-in (dynamic and fixed key/CLABE), cash-out, splits, balance, currency conversion, transaction queries and webhooks. # Introduction Integrate using your `Client ID` and `Client Secret` to create charges (cash in), request withdrawals (cash out) and check status. Optionally, provide a `webhook_url` to receive automatic notifications. Tip: generate your key at Dashboard → API. Keys are created with the required initial permissions. **Base URL** ``` https://api.zypher.lat ``` ### First steps - Generate your credentials in Dashboard → API (Client ID and Client Secret). - Create a charge (cash in) in the desired currency, see PIX charge (BRL). - Set your `webhook_url` to receive confirmations automatically. - To pay out, use cash out by PIX key, BR Code or CLABE, see PIX withdrawal (key). # Authentication Send credentials via headers or Basic Auth. **Headers** ``` X-Client-Id: Client ID X-Client-Secret: Client Secret ``` **Basic Auth** ``` Authorization: Basic base64(ClientID:ClientSecret) ``` Authentication and creating charges (cash in) do NOT require a configured IP. The IP restriction (IPv4 in the key allowlist) applies only to cash out via API. # Sandbox The Sandbox lets you validate your entire integration (create a charge, receive the webhook, check status) WITHOUT moving real money. Nothing is actually confirmed, it is 100% isolated. A request with the Test Key NEVER touches a real acquirer, balance or transaction. Responses have the same format as production (+ the `"sandbox": true` field). ### Test Key (public) Use these ready-to-go credentials, no need to generate anything. They are public on purpose (the sandbox moves no money). **Headers** ``` X-Client-Id: xpagsandbox_00000000 X-Client-Secret: 202620262026202620262026 ``` ### 1) Create a test charge POST `/cashin` · same route as production. Change `currency` to `BRL` · `MXN` to test each currency. **Body** ``` { "currency": "BRL", "amount": 100.00, "external_id": "TESTE-001", "webhook_url": "https://seusistema.com/webhooks/zypherglobal" } ``` **Response** ``` { "sandbox": true, "ok": true, "status": "pending", "currency": "BRL", "amount": 100, "fee": 3, "request_number": "sbx_...", "transaction_id": "SBX-...", "copy_paste": "00020101...TEST...", "expire_minutes": 30 } ``` Keep the `transaction_id`, you use it to simulate the payment in the next step. Each currency returns its matching payment instruction in the response (e.g. `clabe` for MXN, `payee_data` for COP). ### 2) Simulate the payment (fires the webhook) POST `/sandbox/simulate` · confirms (or fails) the test charge and fires the webhook to your `webhook_url`, identical to production. **Body** ``` { "transaction_id": "SBX-...", "outcome": "paid" } ``` `outcome`: `paid` · `failed` · `expired` — test every scenario of your handler. **Webhook sent to your URL** ``` { "sandbox": true, "type": "cashin", "status": "confirmed", "amount": 100, "fee": 3, "transaction_id": "SBX-...", "external_id": "TESTE-001", "e2e": "E...", "provider": "Zypher Global" } ``` ### Test balance and withdrawal In the sandbox the `/balance` returns a high test balance, so you can test `/cashout` (withdrawal) without real funds. The test withdrawal also accepts `/sandbox/simulate` to simulate settlement. Currency conversion is NOT yet available in the sandbox (it moves real balance). Everything else, cash in, cash out, status, balance and webhook, works. # PIX charge (BRL) POST `/cashin` · `"currency": "BRL"` Single route for all currencies, the currency is set by the `currency` field (default `BRL`). The `amount` field decides the MODE: `com amount` = DYNAMIC charge (fixed amount per charge); `sem amount` = your FIXED PIX KEY (open amount, the payer chooses how much). ### Dynamic charge (by amount) Send `amount` → we return a FIXED-AMOUNT PIX QR for THAT payment. Each charge is unique and disappears once paid/expired. It is the day-to-day flow (checkout, single order). **Body** ``` { "currency": "BRL", "amount": 123.45, "name": "Cliente Pix", "document": "12345678900", "description": "Pedido #123", "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "usuario1", "percentage": 10 }, { "username": "usuario2", "percentage": 5 } ] } ``` **Response** ``` { "ok": true, "qr_img": "data:image/png;base64,...", "copyPaste": "000201...", "code": "...", "request_number": "pr_...", "transaction_id": "pr_..." } ``` Query later by `request_number` (a single payment). See Query Transactions. ### Fixed PIX key (open amount · static) Call the SAME route `/cashin` WITHOUT the `amount` field → we return an OPEN-AMOUNT QR (the payer chooses how much to pay). EACH call generates a NEW QR (its own copy-paste) bound to the `external_id` that YOU send. Authentication, KYC and account are the same as the normal cash-in. **Body** ``` { "currency": "BRL", "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "usuario1", "percentage": 10 } ] } ``` **Response** ``` { "ok": true, "static": true, "open_value": true, "external_id": "PEDIDO-123", "copyPaste": "00020126580014br.gov.bcb.pix...", "qr_url": "https://quickchart.io/qr?text=...", "transaction_id": "acc000094602601" } ``` - The `external_id` is YOURS (you send it, we do NOT generate it). It is bound to the QR and comes back in the webhook. Since each call generates a new QR, create ONE per order → per-order tracking. - Reused the SAME QR across several payments? They all return with the SAME `external_id`, reconcile by `e2e` (unique per payment). The credit is always individual (anti-duplicate lock by `e2e`). - The `origin: "static"` field distinguishes this flow from the dynamic QR in the webhook. Reselling to your own clients? The `external_id` is the order in YOUR system, the amount lands in your balance and you relay the webhook to the end client. Splits, webhook and queries work the same in both modes: Splits (pass-through) · Webhooks · Query Transactions. # SPEI charge (MXN) POST `/cashin` · `"currency": "MXN"` Generates a SPEI/CLABE reference for the payer in Mexico. The `amount` field decides the MODE: `com amount` = DYNAMIC charge (CLABE by amount); `sem amount` = the account FIXED CLABE (open amount). The confirmation arrives at your webhook. ### Dynamic charge (by amount) Send `amount` → we return a CLABE/reference for THAT payment. It is the day-to-day flow (single order). **Body** ``` { "currency": "MXN", "amount": 500.00, "name": "Juan Pérez", "document": "PEPJ800101HDFRRL09", "description": "Pedido #123", "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "operador_mx", "percentage": 10 } ] } ``` **Response** ``` { "ok": true, "reference": "REF123456", "clabe": "012345678901234567", "amount": 500.00, "fee": 5.00, "currency": "MXN", "request_number": "pr_...", "transaction_id": "pr_...", "status": "pending", "bank_name": "STP (Sistema de Transferencia y Pagos)", "beneficiary": "Zypher" } ``` Query later by `request_number`/`transaction_id` (a single payment). See Query Transactions. Every MXN response (dynamic and fixed CLABE) includes `bank_name` and `beneficiary`. Show these on your payment screen next to the CLABE: letting the payer see the bank (`STP`) and the beneficiary name builds trust and boosts SPEI conversion. ### CLABE per sale (open amount · static) Call the SAME route `/cashin` with `"currency": "MXN"` WITHOUT the `amount` field, sending an `external_id` per sale → we return an OPEN-AMOUNT CLABE just for that sale. The same `external_id` returns the SAME CLABE (idempotent on retry); a new `external_id` = a new CLABE. The payer sends any amount via SPEI. Authentication, KYC and active SPEI are the same as the normal cash-in. **Body** ``` { "currency": "MXN", "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "operador_mx", "percentage": 10 } ] } ``` **Response** ``` { "ok": true, "static": true, "open_value": true, "currency": "MXN", "external_id": "PEDIDO-123", "clabe": "012345678901234567", "transaction_id": "spc...", "bank_name": "STP (Sistema de Transferencia y Pagos)", "beneficiary": "Zypher" } ``` - Each sale gets its OWN CLABE (18 digits). Two leads on the SAME account (e.g. Lucas and Maria) NEVER collide, each has its own CLABE and its own `external_id`. One sale never lands on top of another. - The `external_id` is YOURS (you send it, we do NOT generate it; we only echo it in the webhook). It is the reuse KEY: same `external_id` = same CLABE (idempotent); a new `external_id` = a new CLABE. - Each CLABE is OPEN-amount and can receive multiple payments, each one becomes its own webhook with a unique `e2e` (per-payment reconciliation key). - When paid, the `receiverClabe` in the webhook identifies the sale → we fire the webhook with the right `external_id`. The `origin: "static"` field distinguishes this flow from the by-amount deposit. WITHOUT `external_id` → we return a PERMANENT account CLABE (receiving address/wallet); there is no per-sale attribution then, reconcile by `e2e`. To automate sales, ALWAYS send an `external_id` per sale. Reselling to your own clients? The `external_id` is the order in YOUR system, the amount lands in your MXN balance and you relay the webhook to the end client. Since the fixed CLABE has no `transaction_id` per payment, query the LIST of receipts by `clabe`/`external_id` in Query Transactions. Splits and webhook: Splits (pass-through) · Webhooks. ### OXXO (voucher paid in cash) Same route `/cashin` and the SAME MXN balance, but with `"method": "OXXO"`: we generate a voucher the payer takes to any OXXO store and pays in cash. The response already returns the reference and the barcode right away, for you to show on your screen. Accepted range: 10 to 10,000 MXN, and the payer must pay the exact amount. **Body** ``` { "currency": "MXN", "method": "OXXO", "amount": 250.00, "payerData": { "name": "Luis Hernandez", "email": "luis@correo.com" }, "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "operador_mx", "percentage": 10 } ] } ``` - `method` — `"OXXO"`. Required (this is what distinguishes it from SPEI/CLABE). - `amount` — between 10 and 10,000 MXN. Out of range returns `amount_below_min`/`amount_above_limit`. - `payerData` (`name`, `email`) — optional: filled automatically if omitted. - `generateCheckout` — optional (default `true` = hosted page). Use `false` to receive the raw voucher in `payee_data` and show it on your own screen. - `splits` — optional: OXXO supports split just like SPEI/PIX, applied on credit when the payment confirms. See Splits (pass-through). **Response (`generateCheckout: false`)** ``` { "ok": true, "currency": "MXN", "amount": 250.00, "fee": 5.00, "method": "OXXO", "request_number": "oxxo_...", "transaction_id": "oxxo_...", "status": "pending", "awaiting_instruction": false, "transfer_type": "REFERENCE", "payee_data": { "transferType": "REFERENCE", "reference": "8204240000119882", "barcode": "https://static.muwe.mx/.../abc.png" } } ``` - `reference` — the OXXO reference number the payer gives at the register. - `barcode` — image URL (PNG) of the barcode, show it directly on your screen. The voucher stays `pending` until the payer goes to the store (may take hours or days, normal for cash). Confirmation arrives on your webhook when payment is made and credits your MXN balance. Reconcile by `request_number`/`external_id`. **Webhook, when the payer pays at the store** ``` { "type": "cashin", "origin": "dynamic", "status": "confirmed", "amount": 250.00, "fee": 5.00, "request_number": "oxxo_...", "transaction_id": "oxxo_...", "external_id": "PEDIDO-123", "e2e": "E...", "provider": "Zypher Global" } ``` - Fires only when the payment is confirmed (the voucher may stay `pending` for hours/days). Match by `transaction_id`/`request_number`/`external_id`, the same you stored when generating. - If you sent `splits`, the split was already applied on credit before this webhook. Details at Webhooks. # USDT charge POST `/cashin` · `"currency": "USDT"` The payer sends USDT (TRON/TRC20 network) to the `deposit_address`. The response returns the EXACT `amount` to send (the base plus a cents adjustment that identifies the charge). The payer must send exactly that `amount`, otherwise the payment will not reconcile with this order. `splits` apply to the inflow (USDT credited). **Body** ``` { "currency": "USDT", "amount": 100.00, "external_id": "PEDIDO-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal", "splits": [ { "username": "usuario1", "percentage": 10 } ] } ``` **Response** ``` { "ok": true, "currency": "USDT", "network": "TRON", "deposit_address": "T...", "amount": 100.001234, "transaction_id": "PEDIDO-123", "status": "pending" } ``` The payer sends exactly the `amount` in USDT (TRC20) to the `deposit_address`. Confirmation arrives via your `webhook_url` when the payment is confirmed on-chain (`status`=confirmed). You can also query by `transaction_id`. Query by `transaction_id` in Query Transactions. Splits and webhook: Splits (pass-through) · Webhooks. # PIX withdrawal (key) POST `/cashout` · `"currency": "BRL"` Requirements: the request must come from an IPv4 address and the IP must be configured in the `IP` field of the key. Otherwise, the response will be IP not allowed. **PIX_TYPE accepted** ``` EMAIL CPF CNPJ PHONE EVP ``` **Body** ``` { "currency": "BRL", "amount": 250.00, "name": "Fulano de Tal", "document": "00000000000", "pix_key": "fulano@banco.com", "pix_type": "EMAIL", "webhook_url": "https://seusistema.com/webhooks/zypherglobal" } ``` Formatting: for CPF/CNPJ send digits only (no dots/dashes); for PHONE use E.164 (+5511999999999) or digits with country+area code; EMAIL must be a valid address; EVP can be a random key (32 hex) or UUID. **Response** ``` { "ok": true, "e2e": "WD_...", "amount": 250, "fee": 2.5, "status": "pending" } ``` # BR Code withdrawal POST `/cashout` · `pix_copia_cola` Same endpoint as the key-based cash-out (`POST /cashout`), instead of providing `pix_key`/`pix_type`, send the `pix_copia_cola` (BR Code / EMV) that will be PAID. No PIN required (the API is authenticated by the key). Always in BRL. Eligibility: available only for accounts enabled for BR Code (the `pix_qr_enabled` permission). Otherwise the API returns `BR Code cash-out not available for this account.` (`403`). It also requires verified KYC, the `cashout` permission, an authorized IP and cash-out enabled. **Body** ``` { "currency": "BRL", "pix_copia_cola": "00020126580014br.gov.bcb.pix0136...5204000053039865802BR6304ABCD", "amount": 250.00, "description": "Pagamento fornecedor", "webhook_url": "https://seusistema.com/webhooks/zypherglobal" } ``` Field `pix_copia_cola`, accepts the aliases `brcode`, `emv`, `qrcode`. Amount: if the BR Code is STATIC (fixed amount embedded), that amount is used; if you also send `amount` and it differs from the fixed amount, the request is REJECTED (`422 — The amount provided differs from the BR Code fixed amount.`). If the BR Code is DYNAMIC (no embedded amount), send `amount`. **Response** ``` { "ok": true, "e2e": "E20251124...", "transaction_id": "...", "fee": 2.5, "amount": 250, "status": "pending", "currency": "BRL" } ``` Errors: ineligible provider (`403`); missing KYC/permission/IP (`403`); amount differs from the static BR Code (`422`); insufficient balance; invalid/expired BR Code. # SPEI withdrawal (MXN) POST `/cashout` · `"currency": "MXN"` For `MXN` use the `clabe` (18 digits) instead of `pix_key`/`pix_type`. **Body** ``` { "currency": "MXN", "amount": 300.00, "name": "Juan Pérez", "document": "PEPJ800101HDFRRL09", "clabe": "012345678901234567", "webhook_url": "https://seusistema.com/webhooks/zypherglobal" } ``` **Response** ``` { "ok": true, "e2e": "WL_...", "amount": 300, "fee": 3.0, "currency": "MXN", "status": "pending" } ``` # USDT withdrawal POST `/cashout` · `"currency": "USDT"` On-chain USDT withdrawal on the TRON network (TRC20). The `amount` is in USDT, it is the amount sent to `address`. The fee is charged ON TOP, in USDT (debited from your USDT balance). The send is processed and confirmed via your `webhook_url`. Cashout does not accept splits. **Body** ``` { "currency": "USDT", "amount": 100.00, "address": "T...", "network": "TRON", "external_id": "SAQUE-123", "webhook_url": "https://seusistema.com/webhooks/zypherglobal" } ``` **Response** ``` { "ok": true, "transaction_id": "...", "amount": 100.00, "fee": 1.00, "currency": "USDT", "network": "TRON", "status": "pending" } ``` TRC20 only: the `address` must be a valid TRON address (starts with "T"). Wrong network = irreversible loss. The withdrawal stays `status`=pending until the on-chain send is confirmed via your `webhook_url`. # Splits (pass-through) Splits automatically forward part of a cash-in net amount to other accounts on the platform. Send the `splits` field in the cash-in body, it works in every enabled currency, and in BOTH modes (dynamic by amount AND fixed key/CLABE with open amount). **How to send** ``` "splits": [ { "username": "usuario1", "percentage": 10 }, { "username": "usuario2", "percentage": 5 } ] ``` ### Rules - Each item has `username` (destination user on the platform) and `percentage` (percentage of the net amount). - The total sum of percentages, considering Dashboard + payload, cannot exceed 90%. If it does, ALL splits are ignored and 100% of the net stays in the origin account. - They are applied to the NET amount (after the fee), at the very moment the cash-in is confirmed, each destination into its own balance. - Cash-out does NOT accept splits (inflow only). ### By currency - BRL — forwarded in reais, into the destination BRL balance (net after the PIX fee). - MXN — forwarded in pesos, into the destination MXN balance (net after the SPEI fee). - USDT — the split is on the credited USDT, into the destination USDT balance (applied when the on-chain deposit confirms). ### In both modes - Dynamic charge (with `amount`): the splits apply only to THAT payment. - Fixed PIX key / CLABE (without `amount`): the splits are BOUND to the key and applied on every payment it receives. Resending `splits` in a new call updates the key rule. The cascade rule and cap (90%) apply the same in both modes. Details of each flow in Cash In · Charges. # Check Balance GET `/balance` · permission `balance` Returns the balance per currency for the authenticated account. Use it before triggering a withdrawal to confirm there is liquidity. Only currencies enabled in the system are returned. **Response** ``` { "ok": true, "balances": { "BRL": { "available": 1530.75, "blocked": 200.00 }, "MXN": { "available": 0.00, "blocked": 0.00 }, "USDT": { "available": 42.500000, "blocked": 0.000000 } }, "blocked_currencies": ["BRL"], "updated_at": "2026-07-24 12:00:00" } ``` ### Fields - `available` — SPENDABLE amount. It is the only number you should use to decide a payment. - `blocked` — HELD amount (e.g., by an open MED). Informational, explains why the available dropped; do NOT add it to available. - `blocked_currencies` — currencies where holds are tracked. Today only `BRL`: for MXN/USDT the `blocked` is 0 by design, not because "nothing is held". Never treat available + blocked as total spendable balance, the held amount cannot be withdrawn. # Convert Currency Convert your balance between `BRL`, `MXN`, `USDT` at the current rate. Two-step flow: `/conversion/simulate` returns the quote without touching the balance; `/conversion` executes (debits the source, credits the destination). ### 1. Simulate POST `/conversion/simulate` · permission `cashin` **Body** ``` { "amount": 1000.00, "from": "BRL", "to": "MXN" } ``` **Response** ``` { "ok": true, "rate": 3.42, "estimated_amount": 3402.90, "fee": 5.00, "from": "BRL", "to": "MXN" } ``` ### 2. Execute POST `/conversion` · permission `cashin` Same body as the simulation. Requires a verified (KYC) account. The final amount uses the rate at execution time (may differ from the simulation). **Response** ``` { "ok": true, "converted_amount": 3402.90, "rate": 3.42, "fee": 5.00, "from": "BRL", "to": "MXN", "status": "confirmed" } ``` The conversion fee (`fee`) is deducted from the source before applying the rate. Insufficient balance in the source currency returns an error without debiting anything. # Query Transactions Query the REAL state of any transaction, cash-in or cash-out, in any enabled currency, on the dynamic charge (by amount) or on the fixed key/CLABE (open amount). All routes require the `balance` permission and are scoped to the authenticated account. Prefer receiving confirmations via the `webhook_url` (webhook), it is instant. Use the query to RECONCILE (lost webhook, undetermined state). O `/consult-transaction` é UNIVERSAL: aceita qualquer identificador (`request_number`, `transaction_id`, `e2e`, `external_id`) seja de cash-in ou de cash-out e devolve o registro certo no campo `type`. O `/consult-cashout` continua funcionando (compatível com quem já usa). ### Cash In by identifier (dynamic) Dynamic charge (with `amount`) = a single payment. Query by the `request_number` or `transaction_id` returned at creation. **Query** ``` GET /consult-transaction?request_number=pr_... GET /consult-transaction?transaction_id=pr_... ``` **Response** ``` { "ok": true, "type": "cashin", "status": "pending|confirmed|expired", "amount": 123.45, "fee": 1.23, "request_number": "pr_...", "transaction_id": "pr_...", "e2e": "E...", "provider": "Zypher Global", "updated_at": "2026-07-24 12:00:00" } ``` Works for any enabled currency, the currency comes in the record itself. ### Cash In by fixed key/CLABE (static · LIST) The fixed PIX key (BRL) and the fixed CLABE (MXN) receive MULTIPLE payments and you do not have the `transaction_id` of each one. Query by `clabe` OR by `external_id` → returns the LIST of that key receipts. **Query** ``` GET /consult-transaction?clabe=012345678901234567 GET /consult-transaction?external_id=PEDIDO-123 GET /consult-transaction?external_id=PEDIDO-123&limit=20 ``` **Response** ``` { "ok": true, "type": "cashin", "count": 2, "payments": [ { "id": "pr_...", "status": "confirmed", "amount": 100.00, "fee": 1.00, "currency": "MXN", "e2e": "E...", "transaction_id": "...", "external_id": "PEDIDO-123", "clabe": "0123...", "payer_name": "...", "created_at": "...", "updated_at": "..." } ] } ``` For the fixed PIX key (BRL), query by `external_id` (you have no CLABE). For the fixed CLABE (MXN), query by `clabe` or `external_id`. The `limit` limits the list (default 20, max 100). ### Cash Out (withdrawal) Queries the real state of a withdrawal, essential so the cash-out is not left undetermined if a webhook fails. Search by one of these: **Parameters (one of them)** ``` GET /consult-cashout?e2e=E... GET /consult-cashout?transaction_id=pr_... GET /consult-cashout?external_id=SAQUE-123 ``` **Response** ``` { "ok": true, "type": "cashout", "status": "pending|completed|failed", "amount": 250.00, "fee": 2.50, "currency": "BRL", "e2e": "E...", "transaction_id": "pr_...", "external_id": "SAQUE-123", "provider": "Zypher Global", "beneficiary": { "name": "Fulano de Tal", "document": "00000000000", "bank": "NUBANK" }, "pix_key": "fulano@banco.com", "pix_type": "EMAIL", "created_at": "2026-07-24 12:00:00", "updated_at": "2026-07-24 12:00:05", "paid_at": "2026-07-24 12:00:05" } ``` Works for withdrawal in any enabled currency, the currency comes in `currency`. Identifiers come masked as public references (`pr_...`) and the `provider` shows the platform name, internal IDs are never exposed. # Webhooks We will send a POST to your `webhook_url` whenever a transaction changes status. Here is EVERYTHING about webhooks, dynamic charge, fixed key/CLABE (open amount), cash-out and disputes (MED), in the enabled currencies. ### Common fields - `type` — `cashin` | `cashout`. - `status` — `confirmed` · `completed` · `med` · `failed`. - `currency` — transaction currency (`BRL` · `MXN` · `USDT`). - `transaction_id`, `e2e` — identifiers; reconcile by `e2e` (unique per payment). - `external_id` — your identifier, when you sent it at creation. - `origin` — present and equal to `"static"` when the payment landed on a FIXED KEY/CLABE (open amount). Absent = dynamic charge (by amount). The `provider` always comes with the platform name. Always handle the result by `status`, not by the message. ### Cash In (confirmed) ### BRL · PIX **Cash In · BRL** ``` { "type":"cashin", "status":"confirmed", "amount":123.45, "fee":1.23, "currency":"BRL", "request_number":"pr_...", "transaction_id":"pr_...", "e2e":"E...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` ### MXN · SPEI **Cash In · MXN** ``` { "type":"cashin", "status":"confirmed", "amount":500.00, "fee":5.00, "currency":"MXN", "request_number":"pr_...", "transaction_id":"pr_...", "e2e":"E...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` ### USDT **Cash In · USDT (BRL→USDT)** ``` { "type":"cashin", "status":"confirmed", "amount":19.23, "fee":0.58, "currency":"USDT", "transaction_id":"CRYPTO-...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` In BRL→USDT the webhook fires as soon as the PIX confirms, the `amount` is the ESTIMATED USDT (the real amount is credited at the end of the conversion). ### Fixed key / open amount (static) Payments on a FIXED KEY (the fixed PIX QR in BRL or the fixed CLABE in MXN) arrive with `"origin":"static"` and your `external_id` back. Since the same key receives multiple payments, each event is individual, reconcile by `e2e`. **Cash In · fixed key/CLABE** ``` { "type":"cashin", "origin":"static", "status":"confirmed", "amount":100.00, "currency":"MXN", "external_id":"PEDIDO-123", "transaction_id":"PEDIDO-123", "e2e":"E...", "provider":"Zypher Global" } ``` Same format for BRL (fixed PIX QR) and MXN (fixed CLABE), only the `currency` changes. ### Cash Out (completed) **Cash Out · BRL** ``` { "type":"cashout", "status":"completed", "amount":250.00, "fee":2.50, "currency":"BRL", "transaction_id":"pr_...", "external_id":"SEU-ID-123", "e2e":"E...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` **Cash Out · MXN** ``` { "type":"cashout", "status":"completed", "amount":300.00, "fee":3.00, "currency":"MXN", "transaction_id":"pr_...", "e2e":"E...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` **Cash Out · USDT (USDT→BRL)** ``` { "type":"cashout", "status":"completed", "usdt":20.00, "brl":100.00, "fee":0.50, "currency":"USDT", "transaction_id":"pr_...", "e2e":"E...", "provider":"Zypher Global", "updated_at":"2025-11-24 12:00:00" } ``` ### Cash Out (failed / canceled) If an already-accepted withdrawal (returned as `pending`) is NOT completed, declined by the recipient bank, invalid key, expired QR, or no liquidity in time, we send a POST with `"status":"failed"` and the `fail_reason`. The amount (`amount` + `fee`) is automatically refunded to your balance; use the webhook to re-credit or reprocess your end user. **Cash Out · falha** ``` { "type":"cashout", "status":"failed", "amount":250.00, "fee":2.50, "transaction_id":"pr_...", "external_id":"SEU-ID-123", "e2e":"E...", "fail_reason":"Invalid PIX key.", "updated_at":"2025-11-24 12:00:00" } ``` The `external_id` (the same one you sent in the cash-out) comes in ALL withdrawal callbacks, `completed` and `failed`, to match the transaction on your side. The synchronous rejection (invalid data at POST time) already returns in the HTTP of the call itself; this `failed` webhook covers the asynchronous failure (after `pending`). ### MED / Dispute (BRL) MED (PIX dispute): when an already-confirmed inflow is contested by the payer, we send a POST with `"status":"med"` on the SAME `transaction_id` as the original charge. Use it to lock or reverse the order on your side, the net amount is debited/held again in your balance. **MED · BRL** ``` { "type": "cashin", "status": "med", "amount": 123.45, "fee": 1.23, "currency": "BRL", "request_number": "pr_...", "transaction_id": "pr_...", "e2e": "E...", "provider": "Zypher Global", "updated_at": "2025-11-24 12:00:00", "med": { "payer_name": "João da Silva", "payer_document": "***.456.789-**", "payer_institution": "ISPB 12345678", "infraction_type": "FRAUD", "infraction_status": "OPEN", "infraction_reason": "Pagador alega não reconhecer a transação", "reference_id": "PROTOCOL-123", "reported_by": "PAGADOR" } } ``` The `med` object carries the infraction details (when the acquirer provides them); fields may come empty/absent, treat them as optional. Idempotency: always reconcile by `e2e` (unique per payment), the same fixed key/CLABE resends the same `external_id` on different payments. If the webhook fails, query the real state in Query Transactions. # Error handling Every API failure returns a standardized JSON body with a human-readable message and a stable `error_code`. The HTTP status also reflects the error (not always 200). Handle errors by `error_code`, it does NOT change across languages or versions. **Error format** ``` HTTP 422 { "ok": false, "error": "Chave PIX não encontrada.", "error_code": "pix_key_not_found" } ``` - `ok` — always `false` on error. - `error` — human-readable message (localized). Good to display/log; do NOT use for logic. - `error_code` — stable slug to handle in code (switch/if). This is the source of truth. ### HTTP Status - 400 — invalid request (data/fields). - 401 — missing or invalid credentials. - 403 — no permission, IP not allowed, or resource disabled. - 422 — data rejected during processing (e.g., key not found, insufficient balance). - 429 — too many attempts. Wait and try again. - 500 / 503 — internal error or service temporarily unavailable. The status is also sent in the `X-App-Error-Code` header (for clients that only read headers). ### Error codes The same `error_code` can appear on different routes. Always handle by the code. ### Authentication and access - `missing_credentials`401— Client ID/Secret missing in the header. - `invalid_credentials`401— Invalid Client ID/Secret. - `key_inactive`403— API key is inactive. - `permission_denied`403— The key lacks the required permission (cashin/cashout/balance). - `ip_not_configured`403— Cash-out without an IP in the key allowlist. - `ip_not_allowed`403— Source IP not in the allowlist (or not IPv4). - `invalid_account`403— Account linked to the key is invalid. - `rate_limited`429— Too many requests in a short time. ### Key / recipient (PIX) - `pix_key_not_found`422— The PIX key does not exist in DICT. - `pix_key_invalid`422— PIX key has an invalid format. - `pix_key_type_invalid`422— Key type not accepted for this destination. - `invalid_document`422— CPF/CNPJ invalid or does not match the key. - `name_mismatch`422— Recipient name/document does not match the key. - `recipient_cannot_receive`422— Destination account cannot receive (closed/inactive). - `destination_blocked`422— Destination key or account blocked/suspended. ### Amount and balance - `insufficient_balance`400— Insufficient balance for the amount + fee. - `amount_below_min`422— Amount below the minimum allowed. - `amount_above_limit`422— Amount above the allowed limit. - `amount_unavailable`422— No rail covers this amount right now, try a lower amount. - `invalid_amount`422— Invalid or zero amount. ### Processing / provider - `duplicate`422— Duplicate operation (check if it was already processed). - `unauthorized`403— Operation not authorized by the provider. - `maintenance`503— Service under maintenance. - `provider_unavailable`422— Service unavailable or timeout, you can retry. - `cashin_failed`422— Generic failure creating the charge. - `cashout_failed`422— Generic withdrawal failure (contact support). ### Account / permissions - `kyc_required`403— Account without document verification (KYC). - `currency_not_supported`400— Currency not supported (use BRL, MXN, ARS, COP or USDT). - `currency_not_enabled`403— Currency not enabled for this account, contact support. - `phone_required`422— Buyer phone required for COP cash-in (NEQUI/BRE-B). ### Conversion - `invalid_request`400— Fields amount/from/to missing or invalid. - `conversion_failed`500— Failed to execute the conversion (nothing was debited). - `conversion_in_progress`409— A conversion with that external_id is already in progress, wait, do not resend. ### Lookup / status - `missing_reference`400— Missing identifier (request_number / e2e / transaction_id). - `deposit_not_found`404— No deposit for the given identifier. - `withdrawal_not_found`404— No withdrawal for the given identifier. Retryable: `provider_unavailable` and `rate_limited` are transient, you can repeat the same request (with the same `external_id` to avoid duplicates). The rest are final: fix the data before resending. ### Handling example **JavaScript** ``` const r = await fetch(url, opts); const data = await r.json(); if (!data.ok) { switch (data.error_code) { case 'pix_key_not_found': case 'pix_key_invalid': // pedir outra chave ao usuário break; case 'insufficient_balance': // avisar saldo break; case 'provider_unavailable': case 'rate_limited': // retentar com backoff (mesmo external_id) break; default: // mostrar data.error e logar } } ```