The whole documentation in one file

Copy or download the full reference as Markdown and hand it to your coding assistant to generate the integration in one go.

Quick start

finance.zendry.co

Three steps to your first charge in production. All amounts in cents, dates in ISO 8601 (UTC).

1 · Key
Open your account
Shown only once at creation. Restrict by IP if you can.
2 · Test
GET /balance
Confirms your credential and IP are cleared.
3 · Webhook
Receiving and payout URLs
Payment confirmation arrives by event, not by polling.
First call
200
curl -X GET https://finance.zendry.co/api/public/v1/balance \  -H "Authorization: Bearer gw_sua_chave_aqui"

Authentication

Every request requires your API key in the Authorization: Bearer <key> or x-api-key: <key> header — both formats are accepted. Generate and revoke keys in Integrations.

  • Monetary values always in cents (integer). Dates in ISO 8601 (UTC).
  • With allowed IPs registered in Integrations, calls from other IPs get 403.
  • Payment confirmation is webhook-driven — the GETs are for re-checking, not aggressive polling.

Pix

POST
/api/public/v1/charges

Create Pix charge

Creates a charge and returns the Pix copy-and-paste code. Payment confirmation arrives through the pix.received webhook — do not poll.

  • state: pending → paid, or expired if it lapses unpaid.
  • external_id does not deduplicate: two identical calls create two charges. Store the returned id.
Parameters
FieldTypeRequiredDescription
amount_centsinteger
Yes
Amount in cents. From 1 to 100000000 (R$ 1 million).
descriptiontextNoUp to 140 characters.
external_idtextNoYour identifier. Echoed back in webhooks. Does not deduplicate.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/charges \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "amount_cents": 15000,  "description": "Order #1234",  "external_id": "1234"}'
Response
200
{  "id": "3f1c...",  "state": "pending",  "amount_cents": 15000,  "description": "Order #1234",  "external_id": "1234",  "pix_code": "00020126...5303986540515.00...",  "created_at": "2026-01-01T12:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/charges/:id

Get charge

Returns the current state of a Pix or boleto charge (the same endpoint serves both — the response follows the charge type's format).

  • 404 when the id does not exist or belongs to another account.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/charges/:id \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "3f1c...",  "state": "paid",  "amount_cents": 15000,  "description": "Order #1234",  "external_id": "1234",  "pix_code": "00020126...",  "created_at": "2026-01-01T12:00:00Z",  "paid_at": "2026-01-01T12:04:11Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/payouts

Send Pix (payout)

Debits the available balance and sends a Pix to the given key. Amount + fee are reserved immediately; the final result arrives through the pix.sent webhook.

  • state: sending → completed or failed — notified via the pix.sent webhook; on failure the reserved amount returns to the balance.
  • 409 when the balance cannot cover amount + fee.
  • Idempotency: send the Idempotency-Key header. Retrying with the SAME key returns the payout already created, without duplicating the debit.
  • Without that header, do not retry after a timeout before calling GET /payouts/{id}: the debit may already be reserved.
Parameters
FieldTypeRequiredDescription
amount_centsinteger
Yes
Amount in cents. From 1 to 100000000.
pix_keytext
Yes
Recipient's Pix key.
pix_key_typecpf | cnpj | email | phone | evpNoWhen omitted, the type is inferred from the key's format.
descriptiontextNoUp to 140 characters.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/payouts \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "amount_cents": 5000,  "pix_key": "customer@email.com",  "pix_key_type": "email",  "description": "Payout"}'
Response
200
{  "id": "9ac2...",  "state": "sending",  "amount_cents": 5000,  "fee_cents": 149,  "reserved_cents": 5149,  "idempotent_id": "b7e1...",  "created_at": "2026-01-01T12:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/payouts/{id}

Get a payout

Returns the current state of a payout by the id from creation. Use it when the pix.sent webhook delivery fails, or to reconcile.

  • 404 when the id does not exist or belongs to another account. A test key only sees sandbox payouts.
  • failure_reason is only set on failed, with one of four Portuguese phrases: "Chave Pix inválida." (invalid Pix key), "Saque cancelado." (payout canceled), the sandbox simulated failure, or "Não foi possível concluir a operação. Tente novamente em alguns instantes." (could not complete; try again). It is display text, not something to parse. end_to_end_id only on completed.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/payouts/{id} \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "9ac2...",  "state": "failed",  "amount_cents": 5000,  "fee_cents": 149,  "reserved_cents": 5149,  "pix_key": "customer@email.com",  "pix_key_type": "email",  "description": "Transfer",  "idempotent_id": "b7e1...",  "end_to_end_id": null,  "failure_reason": "Não foi possível concluir a operação. Tente novamente em alguns instantes.",  "created_at": "2026-01-01T12:00:00Z",  "completed_at": null,  "failed_at": "2026-01-01T12:00:07Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/payouts

List payouts

Lists the account payouts, newest first. Filter by status to find the ones still in flight.

  • status=sending returns what has no outcome yet — this is the call to reconcile when the webhook never arrived.
  • 422 when status is not one of the three.
Parameters
FieldTypeRequiredDescription
statussending | completed | failedNoWithout this field, returns all of them.
limitintegerNoFrom 1 to 100. Defaults to 50.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/payouts \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "payouts": [    {      "id": "9ac2...",      "state": "sending",      "amount_cents": 5000,      "fee_cents": 149,      "reserved_cents": 5149,      "pix_key": "customer@email.com",      "pix_key_type": "email",      "description": "Transfer",      "idempotent_id": "b7e1...",      "end_to_end_id": null,      "failure_reason": null,      "created_at": "2026-01-01T12:00:00Z",      "completed_at": null,      "failed_at": null    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Boleto

POST
/api/public/v1/charges/boleto

Create boleto charge

Issues a boleto (Brazilian bank slip) for the given buyer. Confirmation arrives through the boleto.paid webhook (bank clearing, usually D+1).

  • Requires card enabled for the account.
  • data_limite is the last day the boleto is still accepted after the due date. linha_digitavel is the typeable line; codigo_barras the barcode.
  • pdf_url is the boleto page, to print or save as PDF.
  • Query it through the same GET /charges/:id.
Parameters
FieldTypeRequiredDescription
amount_centsinteger
Yes
Amount in cents.
due_datedate (YYYY-MM-DD)
Yes
Boleto due date.
buyerobject
Yes
first_name, last_name, email and taxpayer_id (CPF or CNPJ).
buyer.addressobjectNoline1, neighborhood, city, state, postal_code. Optional as a whole.
descriptiontextNoUp to 140 characters.
external_idtextNoYour identifier.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/charges/boleto \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "amount_cents": 15000,  "description": "Order #1234",  "external_id": "1234",  "due_date": "2026-01-15",  "buyer": {    "first_name": "Maria",    "last_name": "Souza",    "email": "maria@email.com",    "taxpayer_id": "12345678900",    "address": {      "line1": "Av. Paulista, 1000",      "neighborhood": "Bela Vista",      "city": "São Paulo",      "state": "SP",      "postal_code": "01310100"    }  }}'
Response
200
{  "id": "3f1c...",  "state": "pending",  "amount_cents": 15000,  "description": "Order #1234",  "external_id": "1234",  "due_date": "2026-01-15",  "deadline_date": "2026-01-17",  "typeable_line": "23791.23456 78900.123456 ...",  "barcode": "23797890000015000123456...",  "pdf_url": "https://finance.zendry.co/boleto/8f1c.../pdf",  "created_at": "2026-01-01T12:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Card

Checkout & 3DS — step by step

Zendry.js

Zendry's 3DS is smart (data-only): for most sales, authentication happens behind the scenes, no screen — just send the buyer's device data. The browser challenge (the bank's iframe) only appears when the issuer demands it, and Zendry.js handles it. The SDK never sees the card number or credentials.

How the data flows
  1. Buyer

    Fills in the card on your checkout page.

  2. Your front end

    Zendry.js collects the device (language, screen, timezone). The card never goes through the SDK.

  3. Your backend

    POST /card_payments with card, device, ip_address and user_agent.

  4. Zendry

    Authenticates with the issuer and responds to your server.

  5. Issuer

    Decides without a screen (data-only) or challenges the buyer.

Response to your server

200 approved

Data-only: authenticated behind the scenes, no screen.

201 requires_action

The issuer asked for a challenge — Zendry.js finishes it in the browser.

402 declined

failure_reason carries the cause. Offer another card.

Webhook card.approved / card.declined

The official outcome. Only release the order here — never from the SDK's return.

Simulador da página de checkout
simulação

É o mesmo arquivo dos dois lados. O envio não sai do simulador: o desfecho abaixo é escolhido por você. Numa venda real, quem confirma é sempre o webhook.

1

Load the SDK and collect the device

Put the script on your checkout page and call Zendry.threeDS() right before submitting the form. It returns a small object with the buyer's language, screen and timezone — no card data goes through the SDK. The whole page is in the simulator above, on the Code tab.

On your checkout page
<script src="https://api.zendry.co/sdk/v1/zendry.js"></script> const threeds = await Zendry.threeDS();// threeds.device → { language, screen_height, screen_width, time_zone_offset }
2

Create the sale on your backend

Your key never reaches the browser: your server is who calls Zendry. Forward the device received from the front end and add the buyer's ip_address and user_agent — they are what sustains screenless authentication.

Your /checkout/pay endpoint
app.post("/checkout/pay", async (req, res) => {  const r = await fetch("https://finance.zendry.co/api/public/v1/card_payments", {    method: "POST",    headers: {      Authorization: `Bearer ${process.env.ZENDRY_API_KEY}`,      "Content-Type": "application/json",    },    body: JSON.stringify({      amount_cents: req.body.amount_cents,      installments: 1,      external_id: req.body.order_id,      buyer: req.body.buyer,      card: req.body.card,      device: req.body.device,      ip_address: req.headers["x-forwarded-for"]?.split(",")[0],      user_agent: req.headers["user-agent"],    }),  });  const sale = await r.json();   switch (sale.status) {    case "aprovada":      return res.json({ status: "approved", id: sale.id });    case "requires_action":      // return the action to the front end — Zendry.js finishes the challenge      return res.json({ status: "requires_action", action: sale.action, id: sale.id });    default:      return res.status(402).json({ status: "declined", reason: sale.failure_reason });  }});
3

Finish the challenge when you get 201 requires_action

The action is opaque: return it to your front end exactly as received and call Zendry.completeThreeDS. Without an actual challenge (frictionless), it resolves in seconds, no screen.

In the browser
201
const outcome = await Zendry.completeThreeDS(sale.action); if (outcome.status === "authorized") showSuccess();else if (outcome.status === "expired") askToRetry();else showDecline(); // this is UX. Only release the order after your server's confirmation.
4

Confirm the outcome on the server

The SDK's return is for the screen; the truth is the webhook (or the GET /api/public/v1/card_payments/:id). Validate the HMAC signature before releasing any order.

Receiving card.approved
import crypto from "node:crypto"; app.post("/webhooks/zendry", express.raw({ type: "*/*" }), (req, res) => {  const signature = req.headers["x-zendry-signature"];  const expected = crypto    .createHmac("sha256", process.env.ZENDRY_WEBHOOK_SECRET)    .update(req.body)    .digest("hex");   if (signature !== expected) return res.status(401).end();   const event = JSON.parse(req.body.toString());  if (event.event === "card.approved") releaseOrder(event.data.external_id);  if (event.event === "card.declined") cancelOrder(event.data.external_id);   res.status(200).end();});
Outcomes
FieldTypeRequiredDescription
aprovada (200)show successNoThe card.approved webhook confirms. Release the order there.
requires_action (201)call completeThreeDSNoAfter the challenge, card.approved or card.declined arrives. Without completion in 15 min, the sale expires.
recusada (200)offer another cardNofailure_reason carries the cause. Webhook card.declined.
422fix the requestNoerror_type names the cause (e.g. INVALID_REQUEST for a missing device under reinforced posture).
  • Send device on EVERY sale: it raises approval rates, and accounts under reinforced risk posture reject the request without it (422).
  • action.session_id is single-use and expires in 15 minutes — if the buyer doesn't finish, the sale expires and is declined.
  • Never release the order from the SDK's return: only the webhook (or the GET) is the official result.
  • Do not reuse the same action on another attempt — create a new sale.
Try it

In the sandbox the card is simulated without real 3DS: any valid number approves and 4000 0000 0000 0002 declines (use the Card payment endpoint's console). The real 3DS challenge only happens in production.

POST
/api/public/v1/card_payments

Card payment

Charges a credit card server-to-server. Requires card enabled for the account. The card number is never stored.

  • Sandbox (gw_test_ key): authorization is simulated — any valid card number APPROVES and 4000 0000 0000 0002 DECLINES; no 3DS; refunds are simulated too.
  • 201 + status requires_action: finish in the browser with Zendry.completeThreeDS(response.action) — see the Checkout & 3DS guide above. The official outcome arrives through the card.approved / card.declined webhook and the GET below.
  • A decline is NOT an HTTP error: it comes as 200 with state declined and a readable failure_reason.
  • Business errors come as 422 in the { error, error_type, error_code, retry } format — catalog in the Errors & limits section.
  • If your platform resells to merchants (chained gateway), send the END buyer's ip_address and user_agent — the HTTP headers carry your server, not the buyer.
Parameters
FieldTypeRequiredDescription
amount_centsinteger
Yes
Amount in cents.
installmentsintegerNo1 to 12. Default 1.
buyerobject
Yes
name, email and taxpayer_id (CPF, 11 digits).
cardobject
Yes
holder_name, number, expiration_month (MM), expiration_year (YYYY) and security_code.
billingobjectNoBilling address.
deviceobjectNoBuyer's device data collected by Zendry.js (Zendry.threeDS()). Raises approval rates — and accounts under reinforced risk posture REQUIRE this field. Fields: language, screen_height, screen_width, time_zone_offset (hours, Brazil = -3).
ip_addresstextNoThe BUYER's IP. Required in practice for chained integrations.
user_agenttextNoThe BUYER's user-agent.
external_idtextNoYour identifier.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/card_payments \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "amount_cents": 15000,  "installments": 1,  "external_id": "1234",  "buyer": {    "name": "Maria Souza",    "email": "maria@email.com",    "taxpayer_id": "12345678900"  },  "card": {    "holder_name": "MARIA SOUZA",    "number": "4111111111111111",    "expiration_month": "12",    "expiration_year": "2030",    "security_code": "123"  },  "ip_address": "203.0.113.10",  "user_agent": "Mozilla/5.0 ..."}'
Response — approved or declined
200
{  "id": "7d21...",  "charge_id": "1b8e...",  "state": "approved",  "amount_cents": 15000,  "installments": 1,  "external_id": "1234",  "created_at": "2026-01-01T12:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/card_payments/:id

Get card transaction

Returns the transaction by the id given at creation — including to follow the outcome of a 3DS challenge.

  • state: approved | declined | refunded | requires_action.
  • nsu: the receipt NSU. null when the sale never reached the card network (declined before authorization, sandbox).
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/card_payments/:id \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "7d21...",  "charge_id": "1b8e...",  "state": "approved",  "amount_cents": 15000,  "gross_cents": 15000,  "fee_cents": 899,  "net_cents": 14101,  "installments": 1,  "external_id": "1234",  "failure_reason": null,  "nsu": "Z000186-1049...",  "refunded_at": null,  "created_at": "2026-01-01T12:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/card_payments/:id/refund

Refund transaction

Full refund. Available only for approved transactions and within 24 hours of approval.

  • After 24h, or on a non-approved transaction: 422 with the reason in the error field.
  • The card.refunded event fires on the receiving webhook.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/card_payments/:id/refund \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "7d21...",  "state": "refunded",  "refunded_at": "2026-01-01T18:22:40Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Crypto (USDT)

POST
/api/public/v1/crypto_payouts

Convert to USDT and send (crypto payout)

Converts the amount in BRL to USDT at the current rate and sends it to the given wallet. The BRL leaves the balance immediately; the USDT stays blocked until the on-chain confirmation, delivered by the crypto.sent webhook.

  • state: requested → completed (crypto.sent webhook, with tx_hash) or failed (crypto.failed webhook; the USDT returns to the available USDT balance — the BRL conversion is not undone).
  • usdt_amount is what reaches the wallet: the converted USDT minus the USDT outbound fee (fee_usdt_minor). applied_rate is the conversion rate: BRL paid per USDT converted (amount_cents ÷ (usdt_amount_minor + fee_usdt_minor), in each currency's units). Sending from the USDT balance involves no conversion: applied_rate is 0.
  • 409 when the BRL balance is insufficient. 422 when the BRL → USDT conversion is not enabled for the account or the amount breaks a payout limit.
  • Idempotency: send the Idempotency-Key header. Repeating the call with the SAME key returns the payout already created, without converting again.
  • Sending from the USDT balance: send usdt_amount_minor instead of amount_cents. This is the way to resend a failed payout (the USDT returns to the balance, not to BRL). amount_cents in the response is 0 in that case.
  • Sandbox: the outcome is immediate — an amount ending in 99 cents simulates a failure, any other completes without tx_hash (the sandbox never reaches the network; network identifiers are never made up).
Parameters
FieldTypeRequiredDescription
amount_centsinteiroNoAmount in BRL, in cents. From 1 to 100000000. The account's payout minimum, maximum and daily cap apply.
usdt_amount_minorinteiroNoAlternative to amount_cents: sends USDT already in your balance (minor unit, 6 decimals) without converting. Send exactly one of the two.
wallettexto
Yes
Destination wallet address on the chosen network.
networktrc20 | erc20 | bep20 | polygon
Yes
Network for the transfer: TRON (TRC20), Ethereum (ERC20), BNB Smart Chain (BEP20) or Polygon.
descriptiontextoNoUp to 140 characters.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/crypto_payouts \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "amount_cents": 50000,  "wallet": "TKcXiZ1ovginwWvoCHZwwSPn7ZUhCA7Ndg",  "network": "trc20",  "description": "Repasse"}'
Response
200
{  "id": "5b7d...",  "state": "requested",  "amount_cents": 50000,  "usdt_amount": 94.754323,  "usdt_amount_minor": 94754323,  "fee_usdt_minor": 0,  "applied_rate": 5.276804,  "wallet": "TKcXiZ1ovginwWvoCHZwwSPn7ZUhCA7Ndg",  "network": "trc20",  "description": "Repasse",  "tx_hash": null,  "failure_reason": null,  "conversion_id": "c0a1...",  "created_at": "2026-01-01T12:00:00Z",  "completed_at": null,  "failed_at": null}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/crypto_payouts/{id}

Get crypto payout

Returns the current state of a USDT payout by the id from creation. Use it when the crypto.sent webhook delivery failed or to reconcile.

  • 404 if the id does not exist or belongs to another account. A test key only sees sandbox payouts.
  • tx_hash is only set in completed; failure_reason only in failed.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/crypto_payouts/{id} \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "5b7d...",  "state": "completed",  "amount_cents": 50000,  "usdt_amount": 94.754323,  "usdt_amount_minor": 94754323,  "fee_usdt_minor": 0,  "applied_rate": 5.276804,  "wallet": "TKcXiZ1ovginwWvoCHZwwSPn7ZUhCA7Ndg",  "network": "trc20",  "description": "Repasse",  "tx_hash": "7f3a9c...e21b",  "failure_reason": null,  "conversion_id": "c0a1...",  "created_at": "2026-01-01T12:00:00Z",  "completed_at": "2026-01-01T12:03:40Z",  "failed_at": null}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/crypto_payouts

List crypto payouts

Lists the account's USDT payouts, newest first. Filter by status to find what is still in progress.

  • status=requested returns what has no outcome yet — this is the call to reconcile when the webhook never arrived.
  • 422 when status is not one of the three.
Parameters
FieldTypeRequiredDescription
statusrequested | completed | failedNoWithout this field, returns all.
limitinteiroNoFrom 1 to 100. Default 50.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/crypto_payouts \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "crypto_payouts": [    {      "id": "5b7d...",      "state": "requested",      "amount_cents": 50000,      "usdt_amount": 94.754323,      "usdt_amount_minor": 94754323,      "fee_usdt_minor": 0,      "applied_rate": 5.276804,      "wallet": "TKcXiZ1ovginwWvoCHZwwSPn7ZUhCA7Ndg",      "network": "trc20",      "description": "Repasse",      "tx_hash": null,      "failure_reason": null,      "conversion_id": "c0a1...",      "created_at": "2026-01-01T12:00:00Z",      "completed_at": null,      "failed_at": null    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Disputes

GET
/api/public/v1/disputes

List disputes

Disputes (MED and chargebacks) on your account, newest first. Opening is announced by the dispute.opened webhook; use this list to follow their state.

  • The dispute amount is always the whole transaction — there is no partial dispute.
  • While alive, the amount is held from the available balance; the outcome arrives via the dispute.closed webhook (outcome won/lost).
Parameters
FieldTypeRequiredDescription
statusopen | closed | exactNoopen = alive (open, under_defense, under_review); closed = won and lost; or an exact state.
limitintegerNoFrom 1 to 100. Default 50.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/disputes \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "disputes": [    {      "id": "19ba...",      "type": "med",      "state": "open",      "amount_cents": 15000,      "currency": "BRL",      "transaction_reference": "469208464",  "transaction_identifier": "d6f1849fe1654d83a4049db495f26ee8",  "charge_id": "94a9f429-dc23-4b32-861a-351499d91790",      "transaction_identifier": "d6f1849fe1654d83a4049db495f26ee8",      "charge_id": "94a9f429-dc23-4b32-861a-351499d91790",      "reason": "MED REFUND_REQUEST",      "opened_at": "2026-01-01T12:00:00Z",      "defense_deadline": "2026-01-08T12:00:00Z",      "responded_at": null,      "closed_at": null    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/disputes/:id

Get dispute

Full detail: dispute data, message history (author: seller = you, zendry = us) and evidence already attached.

  • 404 when the id does not exist or belongs to another account.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/disputes/:id \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "19ba...",  "type": "med",  "state": "under_defense",  "amount_cents": 15000,  "currency": "BRL",  "transaction_reference": "469208464",  "transaction_identifier": "d6f1849fe1654d83a4049db495f26ee8",  "charge_id": "94a9f429-dc23-4b32-861a-351499d91790",  "reason": "MED REFUND_REQUEST",  "opened_at": "2026-01-01T12:00:00Z",  "defense_deadline": "2026-01-08T12:00:00Z",  "responded_at": "2026-01-02T09:30:00Z",  "closed_at": null,  "messages": [    { "author": "seller", "text": "Service delivered, receipt attached.", "created_at": "2026-01-02T09:30:00Z" }  ],  "evidence": [    { "name": "delivery-receipt.pdf", "content_type": "application/pdf", "size_bytes": 182044, "created_at": "2026-01-02T09:29:00Z" }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/disputes/:id/defense

Submit defense

Records your written defense and moves the dispute to under_defense. Attach evidence before or after — the defense can be supplemented while the dispute is alive.

  • 422 when the dispute is already closed (won or lost).
Parameters
FieldTypeRequiredDescription
texttext
Yes
The defense argument. From 5 to 4000 characters.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/disputes/:id/defense \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "text": "Service delivered on 01/01, customer accepted in the app. Receipt attached."}'
Response
200
{  "id": "19ba...",  "state": "under_defense"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/disputes/:id/evidence

Attach evidence

Uploads ONE evidence file (delivery receipt, acceptance, customer conversation). The body is multipart/form-data with the `file` field — the file itself, not base64. For several files, repeat the call.

  • Example: curl -X POST .../disputes/{id}/evidence -H "Authorization: Bearer …" -F "file=@receipt.pdf"
  • 413 above 15MB; 415 when the format is not one of the accepted ones; 422 when the dispute is already closed.
Form-data
FieldTypeRequiredDescription
filefile
Yes
Up to 15MB. PDF, image (PNG, JPEG, GIF, WEBP, HEIC), document (DOC, DOCX, ODT, RTF, TXT) or spreadsheet (XLS, XLSX, ODS, CSV).
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/disputes/:id/evidence \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "19ba...",  "name": "delivery-receipt.pdf",  "size_bytes": 182044}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/disputes/:id/accept

Do not contest

Declares you will not defend this dispute. It proceeds to review and the outcome is announced by the dispute.closed webhook.

  • Accepted only for disputes in open or under_defense (422 in other states).
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/disputes/:id/accept \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "19ba...",  "state": "under_review"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Queries

GET
/api/public/v1/balance

Get balance

Returns the available balance of the authenticated account.

Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/balance \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "balance_cents": 84851,  "currency": "BRL"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/statement

Statement

Lists account movements, newest first (up to 200 items).

  • Fixed limit of 200 items per call — paginate by period with from/to.
  • For real-time reconciliation prefer webhooks; the statement is the source for closing and audit.
Query parameters
FieldTypeRequiredDescription
fromdate (YYYY-MM-DD)NoFirst day, Brasília timezone.
todate (YYYY-MM-DD)NoLast day, Brasília timezone.
typetextNoFilters by the responses' "type" field (e.g. pagamento_confirmado).
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/statement \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "transactions": [    {      "id": "9ac2...",      "type": "pagamento_confirmado",      "gross_cents": 15000,      "fee_cents": 299,      "net_cents": 14701,      "end_to_end_id": "E18236120202601011204a1b2c3",      "description": "Order #1234",      "counterparty": "Maria Souza",      "created_at": "2026-01-01T12:04:11Z"    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Webhook endpoints

GET
/api/public/v1/webhook_events

List subscribable events

Catalog of the events an endpoint can subscribe to, with family and description. Use the exact name in the events field.

  • Only events that already have an emitter are listed — the catalog grows without breaking existing subscriptions.
  • "*" is not listed but is accepted in events: it subscribes to every event, including future ones.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/webhook_events \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "events": [    { "event": "pix.received", "family": "Pix", "description": "Pix charge paid" },    { "event": "pix.sent", "family": "Pix payout", "description": "Pix payout completed" },    { "event": "dispute.opened", "family": "Dispute", "description": "Dispute (MED/chargeback) opened" }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/webhook_endpoints

List endpoints

Every endpoint of the account, including the mirrors of the fixed channels (managed: true).

  • managed: true is the mirror of a fixed channel (receiving, payout, dispute): URL and events change in the Integrations page; the API only reads, tests and lists deliveries.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/webhook_endpoints \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "endpoints": [    {      "id": "e7a1...",      "url": "https://yourstore.com/webhooks/payouts",      "label": "Finance ERP",      "events": ["pix.sent", "crypto.sent", "crypto.failed"],      "active": true,      "dialect": "en",      "managed": false,      "consecutive_failures": 0,      "last_delivered_at": "2026-09-05T12:04:12Z",      "last_failed_at": null,      "created_at": "2026-09-05T11:00:00Z",      "updated_at": "2026-09-05T11:00:00Z"    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/webhook_endpoints

Create endpoint

Registers a URL and the events it receives. The same event can go to several URLs; every delivery is signed with the account secret.

  • Up to 10 endpoints per account (fixed-channel mirrors don't count) — 422 webhook_endpoint_limit_reached.
  • 422 webhook_url_invalid for a URL outside the rule; 422 webhook_event_unknown for an event outside the catalog.
  • Test the URL right after creating it (POST /webhook_endpoints/:id/test).
Parameters
FieldTypeRequiredDescription
urltext
Yes
HTTPS with a public host (no private IP, localhost or non-standard port).
eventslist of text
Yes
Catalog names, or ["*"] for all.
labeltextNoDisplay name, up to 80 characters.
activebooleanNoDefault true. Inactive receives nothing.
dialecten | ptNoDefault en. pt delivers the fields in the legacy format (only for accounts migrated from zendry.com).
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/webhook_endpoints \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "url": "https://yourstore.com/webhooks/payouts",  "label": "Finance ERP",  "events": [    "pix.sent",    "crypto.sent",    "crypto.failed"  ]}'
Response
201 Created
{  "id": "e7a1...",  "url": "https://yourstore.com/webhooks/payouts",  "label": "Finance ERP",  "events": ["pix.sent", "crypto.sent", "crypto.failed"],  "active": true,  "dialect": "en",  "managed": false,  "consecutive_failures": 0,  "last_delivered_at": null,  "last_failed_at": null,  "created_at": "2026-09-05T11:00:00Z",  "updated_at": "2026-09-05T11:00:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

PATCH
/api/public/v1/webhook_endpoints/:id

Get, update or delete endpoint

GET returns the endpoint; PATCH changes only the fields sent; DELETE removes it (204, no body).

  • 404 webhook_endpoint_not_found if the id doesn't exist or belongs to another account.
  • 409 webhook_endpoint_managed on PATCH/DELETE of a fixed-channel mirror — change it in the Integrations page.
  • Consecutive failures show in consecutive_failures and last_failed_at; the endpoint is never disabled on its own — watch it and fix the URL.
Parameters
FieldTypeRequiredDescription
urltextNoSame rule as on creation.
eventslist of textNoReplaces the whole list.
labeltextNoUp to 80 characters.
activebooleanNofalse pauses deliveries without deleting the endpoint.
dialecten | ptNoFormat of the data fields.
Request
finance.zendry.co
curl -X PATCH https://finance.zendry.co/api/public/v1/webhook_endpoints/:id \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "events": [    "pix.sent"  ],  "active": true}'
Response
200
{  "id": "e7a1...",  "url": "https://yourstore.com/webhooks/payouts",  "label": "Finance ERP",  "events": ["pix.sent"],  "active": true,  "dialect": "en",  "managed": false,  "consecutive_failures": 0,  "last_delivered_at": "2026-09-05T12:04:12Z",  "last_failed_at": null,  "created_at": "2026-09-05T11:00:00Z",  "updated_at": "2026-09-05T12:30:00Z"}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

POST
/api/public/v1/webhook_endpoints/:id/test

Test endpoint

Delivers a sample event right away, signed like a real delivery, with test: true in the envelope and the X-Zendry-Test header.

  • A single attempt, no retries; the result is also kept in the delivery history.
  • Skip deliveries with test: true in your business processing — the ids in data are fictitious.
Parameters
FieldTypeRequiredDescription
eventtext
Yes
Catalog event whose sample will be sent.
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/webhook_endpoints/:id/test \  -H "Authorization: Bearer gw_your_key_here" \  -H "Content-Type: application/json" \  -d '{  "event": "pix.sent"}'
Response
200
{  "result": "delivered",  "http_status": 200,  "duration_ms": 184,  "response": "ok",  "error": null}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

GET
/api/public/v1/webhook_endpoints/:id/deliveries

Deliveries of an endpoint

Latest delivery attempts to the endpoint, newest first.

  • message_id is the X-Zendry-Webhook-Id of that message: redeliveries and retries share the value.
  • result: delivered, failed or no_destination.
Parameters
FieldTypeRequiredDescription
limitintegerNoQuery string. Default 50, maximum 200.
eventtextNoQuery string. Filters by event.
Request
finance.zendry.co
curl -X GET https://finance.zendry.co/api/public/v1/webhook_endpoints/:id/deliveries \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "deliveries": [    {      "id": "d41c...",      "message_id": "b0d3c1e2-...",      "event": "pix.sent",      "source_event": "pix.sent",      "result": "delivered",      "http_status": 200,      "duration_ms": 184,      "error": null,      "attempt": 1,      "resent_from": null,      "created_at": "2026-09-05T12:04:12Z"    }  ]}
Try it

Create your account to run this call in the sandbox, with a test key and isolated data.

Webhooks

How they work

Configure the fixed channels in Integrations — receiving (charges, card, subscriptions), payout (pix.sent and other outflows) and dispute — or registerendpoints per event, choosing which events each URL receives. We send a JSON POST with the X-Zendry-Event, X-Zendry-Webhook-Id and X-Zendry-Signature: sha256=<hmac> headers.

Envelope
{  "id": "b0d3c1e2-...",  "event": "pix.received",  "sent_at": "2026-01-01T12:04:12Z",  "data": { ...event fields... }}
data examples
{  "charge_id": "3f1c...",  "external_id": "1234",  "amount_cents": 15000,  "description": "Order #1234",  "end_to_end_id": "E1823612...",  "environment": "producao",  "state": "paid"}
Verifying the signature (HMAC-SHA256 over the raw body)
const crypto = require("node:crypto"); function isSignatureValid(rawBody, header, secret) {  const expected =    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");  return (    header?.length === expected.length &&    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))  );}// isSignatureValid(rawBody, req.headers["x-zendry-signature"], SECRET)
  • Respond 2xx within 10 seconds; process asynchronously if needed.
  • On failure (network, timeout or status ≥ 400) we retry up to 3 times, with growing backoff.
  • Every delivery and redelivery is logged in Integrations — from there you can resend a lost event.
  • Handle deliveries idempotently by charge_id/payout_id: redeliveries of the same event can happen.

Endpoints per event

Beyond the fixed channels, register up to 10 URLs and pick the events for each — in the Integrations page or through the API (/api/public/v1/webhook_endpoints). The same event can go to several URLs; events: ["*"] subscribes to everything, including future events.

  • Every delivery carries X-Zendry-Webhook-Id (same value as id in the envelope). Redeliveries repeat it — use it to deduplicate.
  • The fixed channels show up in the list as mirrors (managed: true): URL and events are edited in the fixed fields; there you test them and follow their health.
  • There is a single signing secret per account: the same HMAC applies to every endpoint.
  • Consecutive failures stay visible (consecutive_failures, last_failed_at); nothing is disabled automatically.

Event catalog

21 events
Pix
  • pix.receivedPix charge paid (receiving channel)
  • pix.sentPix payout completed or failed (payout channel)
Boleto
  • boleto.paidBoleto cleared
Card
  • card.approvedTransaction approved (including after 3DS). The nsu field carries the receipt NSU
  • card.declinedTransaction declined
  • card.refundedTransaction refunded
Charge
  • charge.expiredCharge expired unpaid
  • charge.canceledCharge canceled
Disputes
  • dispute.openedDispute (chargeback/MED) opened — infraction webhook
  • dispute.closedDispute closed — infraction webhook
Subscriptions
  • subscription.createdSubscription created
  • subscription.charge_approvedRecurring charge approved
  • subscription.charge_declinedRecurring charge declined
  • subscription.retry_scheduledNew attempt scheduled
  • subscription.retries_exhaustedAttempts exhausted
  • subscription.canceledSubscription canceled
  • subscription.completedSubscription completed
Crypto & International
  • crypto.sentCrypto payout completed
  • crypto.failedCrypto payout failed
  • intl.deposit.receivedInternational deposit received
  • intl.payout.sentInternational payout completed

Sandbox

Test environment

Generate a test key on the Integrations page (prefix gw_test_). It uses the same base URL and the same routes, but operates an isolated environment: test balance, statement and charges stay apart from production and no real money moves — Pix is simulated internally.

  • Coverage: full Pix, boleto and card (creation, query, simulated payment/authorization, refund) — with webhooks and test balance.
  • Boleto: issues with test numbers (fake typeable line/barcode) and is paid through the same POST /charges/:id.
  • Card: simulated without 3DS — any valid number approves; 4000 0000 0000 0002 declines; refunds are simulated too.
  • Webhooks are real (same URL and HMAC signature), with environment: "sandbox" in the payload.
  • Network identifiers (end_to_end_id, nsu, tx_hash) are null in the sandbox: nothing is made up.
  • A test key cannot see production data, and vice versa.
  • Test payouts complete instantly (pix.sent webhook). An amount ending in 99 cents (e.g. 10099) simulates failure — the amount returns to the balance.
POST
/api/public/v1/charges/:id

Simulate payment (sandbox)

Marks the test charge (Pix OR boleto) as paid: credits the sandbox balance and fires the real webhook (pix.received or boleto.paid), HMAC-signed. Test-key only — in production it returns 403.

  • Only works with a gw_test_ key and a sandbox charge (otherwise 403/404).
  • Idempotent: an already-paid charge responds with already_paid: true.
  • An expired charge cannot be paid (422).
Request
finance.zendry.co
curl -X POST https://finance.zendry.co/api/public/v1/charges/:id \  -H "Authorization: Bearer gw_your_key_here"
Response
200
{  "id": "3f1c...",  "state": "paid",  "paid_at": "2026-01-01T12:04:11Z"}
Try it

Create your account to simulate payments in the sandbox with your test key.

Errors & limits

Standard envelope

Every error uses this shape. code is the stable contract — branch on it, never on the message, which is written for the human reading the log and may change. retryable tells you whether repeating the SAME request can succeed, and details shows up only when there is something to show (the invalid field, the limit and the amount you sent).

Shape
422
{  "error": "Amount is above the maximum for this operation",  "code": "amount_above_maximum",  "retryable": false}
Error code catalog
codeHTTPretryableerror (new key)
Authentication and access
missing_credentials
401
falseCredential missing
invalid_credentials
401
falseInvalid credential
ip_not_allowed
403
falseIP not allowed for this key
Invalid request
invalid_json
400422 on the legacy key
falseRequest body is not valid JSON
invalid_payload
422
falseInvalid payload
sandbox_only
422
falseOnly available in the sandbox environment
Limits and amounts
amount_below_minimum
422
falseAmount is below the minimum for this operation
amount_above_maximum
422
falseAmount is above the maximum for this operation
daily_limit_exceeded
422
falseDaily limit exceeded
Balance and blocks
insufficient_balance
409
falseInsufficient balance
Resource and state
charge_not_found
404
falseCharge not found
payout_not_found
404
falsePayout not found
charge_already_paid
422
falseCharge already paid
charge_expired
422
falseCharge expired
charge_finalized
422
falseCharge already finalized
transaction_not_found
404
falseTransaction not found
Payment
payment_method_not_enabled
422
falseThis payment method is not enabled for this account
card_declined
422
falseThe card payment was declined
Disputes and files
dispute_not_found
404
falseDispute not found
dispute_closed
422
falseDispute already closed
dispute_action_not_allowed
422
falseThis dispute no longer accepts this action
file_missing
422
falseSend multipart/form-data with a `file` field
file_too_large
413
falseFile is larger than 15MB
file_type_not_allowed
415
falseSend a PDF, image, document or spreadsheet
Webhooks
webhook_endpoint_not_found
404
falseWebhook endpoint not found
webhook_url_invalid
422
falseWebhook URL must use https and a public host
webhook_event_unknown
422
falseUnknown webhook event
webhook_endpoint_limit_reached
422
falseMaximum number of webhook endpoints reached
webhook_endpoint_managed
409
falseThis endpoint mirrors a fixed channel; edit it in the integration settings
Our fault
service_unavailable
503422 on the legacy key
true
The operation could not be completed. Please try again in a few moments.
internal_error
500422 on the legacy key
true
The operation could not be completed. Please try again in a few moments.
  • Legacy keys keep the Portuguese message and the HTTP status they have today; `code`, `retryable` and `details` are added alongside, so nothing breaks for existing integrations.
  • No internal detail reaches the response. An operation that could not be completed right now becomes `service_unavailable` (503); an unexpected failure on our side, `internal_error` (500). Both say "The operation could not be completed. Please try again in a few moments.", and the reason stays in our log.
  • Before repeating a money-moving POST after a `retryable` error, read the resource with GET — the API does not accept a client idempotency key.

HTTP codes

Status
FieldTypeRequiredDescription
400formatNoBody is not valid JSON.
401authNoMissing, invalid or revoked credential.
403accessNoIP outside the allowlist.
404resourceNoResource missing or owned by another account.
409conflictNoInsufficient balance on a payout, or an endpoint mirroring a fixed channel.
413fileNoDispute evidence larger than 15MB.
415fileNoDispute evidence in a format that is not accepted.
422validationNoInvalid payload, account limit or business rule. Carries details.
500oursNoUnexpected failure on our side; retryable: true.
503unavailableNoThe operation could not be completed right now; try again shortly (retryable: true).
422 — invalid payload
422
{  "error": "Payload inválido",  "details": [    { "path": ["amount_cents"], "message": "Number must be greater than or equal to 1" }  ]}

Card errors

Card routes use their own 422 format — retry tells whether the same request is worth retrying.

Format
422
{  "error": "Meio de pagamento não habilitado para esta conta",  "error_type": "INVALID_REQUEST",  "error_code": "payment_method_not_enabled",  "retry": false}
error_type
FieldTypeRequiredDescription
PAYMENT_DECLINEDretry: falseNoPayment declined; offer another card.
INVALID_REQUESTretry: falseNoInvalid data, or card not enabled for the account (error_code payment_method_not_enabled).
PENDING_CHECKretry: falseNoPayment under verification; wait for the webhook or check the GET before retrying.
TEMPORARY_ERRORretry: trueNoCould not complete right now; try again shortly.
INTERNAL_ERRORretry: trueNoOur failure; try again.

Limits & good practice

Amount per operation
Set per account
Minimum and maximum per method; 100,000,000 cents is the absolute payload cap.
Card
1 to 12 installments
Full refund within 24h of approval.
Texts
140 characters
description and external_id.
Statement
200 items per call
Paginate with from/to.
  • Each account has a minimum and a maximum per method (Pix, card, boleto and payout), and payouts also have a daily cap; USDT payouts have their own maximum and daily cap, in USDT. Outside the range the API rejects with amount_below_minimum, amount_above_maximum or daily_limit_exceeded. The response does not include the limit values: they are shown in the dashboard.
  • Idempotency: the API does not accept a client idempotency key. On a timeout of a money-moving POST (payout, card), check the statement/GET before retrying.
  • Payment confirmation is webhook-driven; if you re-check, limit the frequency (e.g. every 30s) — abusive traffic can be blocked by the firewall.