Refund Guide (PIX Refund)
This guide explains how to request a refund for a received PIX payment.
Overview
The PIX refund allows you to reverse a received payment in full or in part. The original transaction is always identified by its E2E (End-to-End ID) — there is no refund by charge identifier. If you only have the identifier, look up the QR code or the payment first to get the E2E (see Where to Find the E2E).
When to Use
- Duplicate payment - Customer paid twice
- Order cancellation - Order cancelled after payment
- Incorrect amount - Customer paid a different amount than expected
- Operational error - Payment received incorrectly
Deadlines
The PIX refund deadline is set by BACEN and enforced by the settlement bank: 90 days from the original payment, for both ordinary refunds and fraud cases.
Past that deadline the settlement bank refuses the refund — the API forwards
the refusal as partner_rejected (422), with the stated reason in the
partner block. There is no dedicated "deadline expired" error code. In that
scenario, use another reversal method.
Request Refund by E2E (End-to-End ID)
The E2E is the unique transaction identifier in the Brazilian PIX system. Format: E{ISPB}{DATE}{SEQUENTIAL}.
Request
curl -X POST "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/out/refund" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: tenant-suaempresa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-e2e-12345" \
-d '{
"originalEndToEnd": "E36741675202601281435001234567",
"amount": 150.00,
"reason": "user-requested",
"identifier": "refund-order-12345"
}'
Body Parameters
The account that received the original PIX comes from the path ({accountId})
and the currency is always BRL — neither is read from the body.
| Field | Type | Required | Description |
|---|---|---|---|
originalEndToEnd | string | Yes | E2E ID of the original transaction to be refunded |
amount | number | Yes | Amount to refund in BRL — equal to the original PIX amount (full refund) or smaller (partial refund). It cannot exceed it |
reason | string | Yes | Refund reason kebab-case slug (see table below) |
identifier | string | No | Refund identifier (max 38 chars [A-Za-z0-9._-]). Auto-generated when omitted |
description | string | No | Free-form description (max 140 characters) |
reason Values
The list is closed: any value outside this table is rejected with HTTP 400. Slugs are forwarded verbatim to the partner bank (MT Bank), with no translation.
| Slug | When to use |
|---|---|
user-requested | End customer requested the refund (most common case) |
transaction-error | Generic transaction error (wrong amount, inconsistent data) |
unauthorized-transaction | Transaction not authorized by the holder |
fraud | Confirmed/suspected fraud |
trade-disagreement | Commercial dispute (good/service not delivered) |
withdrawal-purchase | Operation involving PIX Saque/Troco |
contractual-divergence | Contractual disagreement between parties |
operational-error | Operational/banking processing error |
duplicate-payment | Duplicate payment |
Success Response
Refund runs through the same pipeline (and shape) as PIX out. The HTTP
status reflects the outcome: 200 (COMPLETED), 422 (FAILED),
202 (TIMEOUT/PENDING).
{
"paymentId": "pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"transactionId": "txn-refund-abc123",
"endToEndId": "D12345678202301011500009876543",
"status": "COMPLETED",
"completedAt": "2026-01-28T15:00:03Z",
"identifier": "refund-order-12345"
}
| Field | Description |
|---|---|
paymentId | Internal payment intent ID (tracking/reconciliation) |
endToEndId | Refund D-code/E2E (once registered) |
status | COMPLETED, FAILED, TIMEOUT, PENDING, PROCESSING |
errorCode / errorReason | set when FAILED |
There is no
refundId/amount/currencyfield — usepaymentIdand track the final outcome via the statement or webhook.
Partial refunds
The amount field is required and defines how much goes back to the payer:
send the original PIX amount to refund it in full, or a smaller value to
refund part of it. Above the original, the API returns
400 refund_amount_exceeded and does not start the refund.
The same PIX can be refunded in more than one installment, up to the original amount. Two rules matter here:
- Each refund needs its own
Idempotency-Keyandidentifier. Reusing either makes the API treat the call as a retry of the previous refund and return that result — the second installment never goes out. - The banking partner owns the refundable balance. CorpX does not add up
partial refunds: a refund that exceeds what is still refundable is rejected
by the partner and comes back as
refund_amount_exceeded.
To find out how much of a PIX has already been refunded, check the account statement: refund entries reference the original transaction's E2E.
Refund Webhook
When the refund is processed, you receive a webhook:
{
"id": "pix-refund-pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"type": "pix.refund.completed",
"occurredAt": "2026-01-28T15:00:03.000000000Z",
"schemaVersion": "1.0",
"environment": "production",
"tenantId": "tenant-yourcompany",
"accountId": "{accountId}",
"data": {
"paymentId": "pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"tenantId": "tenant-yourcompany",
"accountId": "{accountId}",
"status": "SUCCESS",
"endToEnd": "D36741675202601281500009876543",
"transactionId": "txn-refund-abc123",
"amount": 150.00,
"currency": "BRL",
"identifier": "refund-order-12345",
"description": "",
"originalTransactionId": "E36741675202601281435001234567",
"initiatedAt": "2026-01-28T15:00:01.000000000Z",
"completedAt": "2026-01-28T15:00:03.000000000Z",
"payee": {
"name": "John Smith",
"document": "12345678901"
}
}
}
A refused refund emits pix.refund.failed with status: "FAILED" and the
errorCode/errorReason/error fields inside data. A refund in TIMEOUT
emits no webhook — check the statement.
Full Example: Refund Script
#!/bin/bash
# Configuration
API_URL="https://tenant.api.corpx.com"
TOKEN="your_token_here"
TENANT_ID="tenant-suaempresa"
ACCOUNT_ID="your_account"
# Refund by E2E function
refund_by_e2e() {
local e2e=$1
local amount=$2
local reason=$3
echo "Requesting refund..."
echo "E2E: $e2e"
echo "Amount: R$ $amount"
echo "Reason: $reason"
echo ""
IDEMPOTENCY_KEY="refund-$(date +%s)-$RANDOM"
response=$(curl -s -X POST "$API_URL/v1/accounts/$ACCOUNT_ID/pix/out/refund" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "{
\"accountId\": \"$ACCOUNT_ID\",
\"originalEndToEnd\": \"$e2e\",
\"amount\": $amount,
\"currency\": \"BRL\",
\"reason\": \"$reason\"
}")
echo "$response" | jq
}
# Usage
echo "=== PIX REFUND ==="
echo ""
read -p "E2E (End-to-End ID): " e2e
read -p "Amount to refund: " amount
read -p "Reason: " reason
refund_by_e2e "$e2e" "$amount" "$reason"
Where to Find the E2E
The E2E can be found in:
- Payment received webhook response
- Charge lookup after payment
- Account statement (Statement)
- Payer's receipt
Example: Extract E2E from Webhook
{
"id": "evt_in_123",
"type": "pix.in.completed",
"occurredAt": "2026-01-28T14:35:00.000Z",
"schemaVersion": "1.0",
"tenantId": "tenant-yourcompany",
"accountId": "{accountId}",
"data": {
"identifier": "cob_abc123def456",
"endToEnd": "E36741675202601281435001234567",
"amount": 150.00
}
}
The field to store is data.endToEnd.
Example: Extract E2E from Charge (QR Code)
curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/qr-code/lookup?identifier=order-12345" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: tenant-suaempresa"
{
"identifier": "order-12345",
"status": "PAID",
"endToEndId": "E36741675202601281435001234567"
}
The QR lookup response is a flat object (no data envelope); the field to
store is endToEndId.
Common Errors
| Error | HTTP | Cause | Solution |
|---|---|---|---|
missing_fields | 400 | originalEndToEnd or amount missing | Send both fields |
invalid_payload | 400 | reason outside the closed list, or malformed JSON | Use one of the slugs from the table above |
refund_amount_exceeded | 400 | amount greater than the original PIX, or greater than the balance still refundable | Check the statement for how much was already refunded |
original_transaction_not_found | 404 | E2E not found at the settlement bank | Check originalEndToEnd |
conflict | 409 | Transaction has already been fully refunded | Check the history in the statement |
insufficient_funds | 422 | Insufficient balance for the refund | Deposit funds into the account |
policy_denied | 422 | A tenant/account policy rule refused the refund | Check violations in the body — Policies and Rules |
partner_rejected | 422 | Refused by the settlement bank (includes an expired 90-day deadline) | Check the partner block for the stated reason |
partner_error | 502 | Failed to look up the original transaction at the settlement bank | Retry; if it persists, contact support |
The full list is in Errors.
Best Practices
- Save the E2E of all received transactions
- Use Idempotency Key to avoid duplicate refunds — and a different key (and
identifier) for each partial refund of the same PIX - Check the statement for how much of a PIX has already been refunded before requesting another partial refund
- Document the reason for auditing purposes
- Configure webhooks to track the status
- Keep a history of refunds for reconciliation
Next Steps
- Authentication Guide - Obtain access tokens
- QR Code Guide - Create charges
- Webhooks - Receive notifications