kamiPay LogokamiPay Docs

Pay-In Refunds

Refund a Pix charge you received (BRL back to the payer + USDT recovery)

This endpoint refunds a Pay-In (a Pix charge that was paid to you). A refund has two legs:

  1. BRL leg — the original BRL amount (full or partial) is returned to the payer's bank account through the gateway. Identical in every case.
  2. USDT leg — the USDT kamiPay already delivered to you for that charge is recovered. How it is recovered depends on how the charge was settled — see below.

Both legs are tracked independently and reported together in the status endpoint and the refund webhook.

This is for Pay-In refunds — refunding a charge you received. It is unrelated to refunds of Pay-Outs you sent (those appear under Lots with rpt_… ids). Pay-In refunds use rpir_… / rpi_… ids.

The two USDT recovery modes

The response field usdt_recovery_mode tells you which one applied. You do not choose it — kamiPay resolves it from how the original charge was settled.

walletsettlement
Applies tocharges settled individually (the legacy 1:1 flow)charges settled by the batched settlement program, either flavour
Where the USDT comes froman on-chain pull from your per-checkout refund walletdeducted from your next settlement
Refund wallet requiredyes, pre-fundednorefund_wallet_id comes back null
Enabled bysupport, per checkoutenabled by default on enrolled checkouts
Timingrecovery is triggered once the BRL refund is confirmednetted into the next scheduled settlement cycle

Wallet mode — prerequisite, contact support first. To enable refunds on a checkout that settles per-charge you must reach out to kamiPay support. They will assign you a dedicated refund wallet (per checkout) and give you its address. You then have to pre‑fund that wallet with USDT (on Polygon): the refund recovers the USDT from this wallet, so it must hold enough balance (plus a little POL for gas) before you can refund. Without a funded refund wallet, these refund requests are rejected.

Settlement mode — nothing to set up. A batched charge never had a transfer of its own, so there is nothing to pull back: the amount is simply subtracted from your next settlement. No refund wallet, no pre-funding, no balance checks, and none of the wallet-related 409s below can occur. The deduction appears as a refunds[] entry on the settlement detail and the settlement.settled webhook, and is previewed by Pending Charges. See Refunds in batched settlements.

Request Body

You must provide exactly one identifier for the original charge:

NameTypeDescription
operation_idstringThe PIX operation id (ptxr_…) returned as operation_id when you created the charge. One identifier required.
pay_in_transaction_request_idstringAlias of operation_id (same ptxr_… value). One identifier required.
end_to_end_idstringThe end‑to‑end id of the original cash‑in (BACEN E…). One identifier required.
valuefloatOptional. Partial refund amount in BRL, with at most 2 decimal places. Must be > 0 and the original amount. If omitted, the full original amount is refunded.
refund_reasonstringOptional. One of customer_request, duplicated, fraud, internal_error.
detailstringOptional. Free‑text note (max 2000 chars).

Partial refunds are supported. Multiple partial refunds on the same charge are allowed as long as their sum does not exceed the original amount (pending and succeeded refunds both count against that limit).

Example Request

const url = `${baseURL}/v2/pay_in_refunds/request`;

const body = {
  "operation_id": "ptxr_01kr3m9p7n2s4d8h6e5b3t7w1k",
  // "value": 5.05,        // optional: partial refund in BRL; omit for full
  "refund_reason": "customer_request",
  "detail": "duplicated charge"
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${access_token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(body)
});
import requests

url = f"{base_url}/v2/pay_in_refunds/request"

body = {
  "operation_id": "ptxr_01kr3m9p7n2s4d8h6e5b3t7w1k",
  # "value": 5.05,        # optional: partial refund in BRL; omit for full
  "refund_reason": "customer_request",
  "detail": "duplicated charge",
}

headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json',
}

response = requests.post(url, json=body, headers=headers)

Response

A successful request returns 202 Accepted: the refund has been validated and persisted, the BRL refund is being sent to the gateway, and the USDT recovery will be triggered automatically once the BRL refund is confirmed.

wallet mode — the USDT is pulled from your refund wallet:

{
  "pay_in_refund_request_id": "rpir_9e7f2fc2c0b14a8d9a1e3f5b7c2d4e6a",
  "end_to_end_id": "E10573521202606051851hovcb9HXqev",
  "requested_value_brl": "5.05",
  "requested_value_usdt": "1.800000",
  "refund_wallet_id": 12,
  "usdt_recovery_mode": "wallet",
  "refund_reason": "customer_request",
  "detail": "duplicated charge"
}

settlement mode — no wallet is involved; requested_value_usdt will be deducted from your next settlement, so refund_wallet_id is null:

{
  "pay_in_refund_request_id": "rpir_9e7f2fc2c0b14a8d9a1e3f5b7c2d4e6a",
  "end_to_end_id": "E10573521202606051851hovcb9HXqev",
  "requested_value_brl": "5.05",
  "requested_value_usdt": "1.800000",
  "refund_wallet_id": null,
  "usdt_recovery_mode": "settlement",
  "refund_reason": "customer_request",
  "detail": "duplicated charge"
}

Response fields

FieldTypeDescription
pay_in_refund_request_idstringIdentifier of the refund request (rpir_…). Track the refund with it.
end_to_end_idstringEnd-to-end id of the original cash-in.
requested_value_brlstringBRL being returned to the payer.
requested_value_usdtstringUSDT being recovered, prorated at the original charge's own rate.
refund_wallet_idinteger | nullThe refund wallet the USDT is pulled from. null in settlement mode.
usdt_recovery_modestringwallet or settlement — see the two recovery modes.
refund_reasonstring | nullAs supplied in the request.
detailstring | nullAs supplied in the request.

Track the refund afterwards with pay_in_refund_request_id (rpir_…) via the status endpoint, or wait for the refund webhook.

{ "detail": "value out of range" }
{ "detail": "no refundable charge found for the provided identifier" }
// Refunds are not enabled for this checkout (no refund wallet assigned).
// Contact support to enable refunds before using this endpoint.
{ "detail": "refunds not enabled for this checkout" }
// The assigned refund wallet is missing or disabled.
{ "detail": "refund wallet missing or disabled" }
// The refund wallet does not hold enough balance to cover the recovery.
{ "detail": "insufficient USDT balance in refund wallet: available=... required=..." }

The three wallet-related errors above apply to wallet mode only. On a batched-settlement checkout there is no refund wallet, so none of them can occur.

{ "detail": "Incorrect Credentials" }

Validation & errors

The request is rejected (nothing is refunded) when:

StatusReason
400value is ≤ 0, has more than 2 decimals, or exceeds the original amount.
404No refundable charge matches the identifier or the charge belongs to another merchant (both return the same "not found" so a merchant can't probe other merchants' transactions).
409The charge is not merchant‑linked, is itself a refund, or the requested amount would exceed the original (given prior refunds). In wallet mode only, also: refunds are not enabled for the checkout, the original USDT settlement hasn't mined yet, or the refund wallet has insufficient balance (the message states whether USDT or POL/gas is short).
503wallet mode only — the refund wallet balance could not be read on‑chain (transient). Retry later.

In wallet mode, the USDT leg is recovered from your refund wallet, so it must hold enough USDT (to cover the refund, net of any in‑flight recoveries) and enough POL to pay gas. If either is short the request is rejected up‑front with a 409 naming the currency. This does not apply in settlement mode, where the amount is deducted from the next settlement instead.

On this page