Este endpoint permite estornar transações aprovadas, oferecendo suporte a estornos totais e parciais, com cálculo automático de split quando aplicável.
Elegibilidade: Apenas transações com status paid podem ser estornadas. Estornos têm diferentes prazos dependendo do método de pagamento.
Parâmetros da Requisição
UUID da transação a ser estornada Exemplo: pay_abc123def456ghi789
Valor a ser estornado em centavos Omitir para estorno total ou informar valor menor para estorno parcialDeve ser ≤ valor disponível para estorno
Motivo do estorno Valores aceitos:
requested_by_customer - Solicitado pelo cliente
duplicate - Transação duplicada
fraudulent - Suspeita de fraude
product_not_received - Produto não recebido
product_defective - Produto com defeito
service_not_provided - Serviço não prestado
other - Outros motivos
Descrição adicional do motivo do estorno Máximo 255 caracteres - Aparece no extrato do cliente
Como tratar o split no estorno Valores:
proportional (padrão) - Estorna proporcionalmente de cada destinatário
full_main - Remove tudo do destinatário principal
custom - Configuração customizada via split_config
Obrigatório quando split_behavior = "custom"Show Configuração customizada do split
split_config[].recipient_id
ID do destinatário
Valor a ser estornado deste destinatário (em centavos)
Se deve notificar o cliente sobre o estorno Padrão: true
Dados adicionais para associar ao estorno Exemplo: {"order_id": "12345", "support_ticket": "TK-001"}
Exemplos de Requisição
cURL - Estorno Total
cURL - Estorno Parcial
JavaScript - Estorno com Split Customizado
PHP - Estorno Automático
curl -X POST https://zeus-sandbox.autorizou.dev/api/v1/refunds \
-H "Authorization: Bearer 4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_abc123def456ghi789",
"reason": "requested_by_customer",
"description": "Cliente solicitou cancelamento do pedido",
"notify_customer": true,
"metadata": {
"order_id": "ORD-12345",
"support_agent": "agent_001"
}
}'
Resposta
UUID da transação original
Status do estorno Valores: pending, processing, succeeded, failed, cancelled
Valor estornado em centavos
Motivo do estorno informado
Descrição adicional do estorno
Método utilizado para o estorno Valores: credit_card, bank_transfer, pix
Data estimada para o cliente receber o estorno (ISO 8601)
Show Detalhes do split do estorno
split_details[].recipient_id
ID do destinatário
Valor estornado deste destinatário
Status do estorno para este destinatário
Show Informações sobre taxas
Taxa de processamento da transação original
Taxa cobrada pelo estorno (se aplicável)
fees.total_refunded_to_customer
Valor líquido que retornará ao cliente
Dados adicionais fornecidos
Data/hora de criação do estorno (ISO 8601)
Exemplo de Resposta
{
"id" : "ref_def456ghi789jkl012" ,
"payment_id" : "pay_abc123def456ghi789" ,
"status" : "processing" ,
"amount" : 15000 ,
"reason" : "requested_by_customer" ,
"description" : "Cliente solicitou cancelamento do pedido" ,
"refund_method" : "credit_card" ,
"estimated_arrival" : "2024-01-17T10:30:00Z" ,
"split_details" : [
{
"recipient_id" : "rec_marketplace" ,
"amount" : 1500 ,
"status" : "processing"
},
{
"recipient_id" : "rec_seller" ,
"amount" : 13500 ,
"status" : "processing"
}
],
"fees" : {
"processing_fee" : 450 ,
"refund_fee" : 0 ,
"total_refunded_to_customer" : 15000
},
"metadata" : {
"order_id" : "ORD-12345" ,
"support_agent" : "agent_001"
},
"created_at" : "2024-01-15T14:30:00Z"
}
Códigos de Status
Estorno criado com sucesso
Dados inválidos na requisição {
"error" : {
"code" : "validation_error" ,
"message" : "Dados inválidos" ,
"details" : {
"amount" : [ "Valor excede o disponível para estorno" ],
"reason" : [ "Motivo inválido" ]
}
}
}
Transação não encontrada {
"error" : {
"code" : "payment_not_found" ,
"message" : "Transação não encontrada" ,
"details" : {
"payment_id" : "pay_invalid123"
}
}
}
422 - Unprocessable Entity
Transação não elegível para estorno {
"error" : {
"code" : "refund_not_allowed" ,
"message" : "Transação não pode ser estornada" ,
"details" : {
"payment_status" : "pending" ,
"reasons" : [ "payment_not_captured" , "refund_period_expired" ]
}
}
}
Prazos de Estorno
Cartão de Crédito
Transações nacionais: Até 120 dias
Transações internacionais: Até 90 dias
Parceladas: Cada parcela tem prazo individual
Recorrentes: Até a data da última cobrança
⏱️ Tempo de Processamento
Aprovação: Imediata na maioria dos casos
Crédito na fatura: 1-2 faturas seguintes
Notificação: Webhook enviado imediatamente
Confirmação: Email automático ao cliente
PIX
Padrão: Até 90 dias da transação
Suspeita de fraude: Sem limite de tempo
Erro operacional: Até 13 meses
⏱️ Tempo de Processamento
Aprovação: Dentro de 1 hora útil
Crédito na conta: Até 1 hora após aprovação
Disponibilidade: Imediata após crédito
Boleto
Boleto pago: Até 90 dias do pagamento
Boleto não pago: Cancelamento sem custo
⏱️ Tempo de Processamento
Aprovação: Até 1 dia útil
Transferência: D+1 a D+2 úteis
Confirmação: Comprovante por email
Casos de Uso
E-commerce - Cancelamento de Pedido
// Estorno automático quando pedido é cancelado
async function cancelOrder ( orderId , reason ) {
try {
// Buscar pagamento do pedido
const order = await getOrder ( orderId );
const payment = await getPayment ( order . payment_id );
if ( payment . status === 'paid' ) {
const refund = await createRefund ({
payment_id: payment . id ,
reason: reason || 'requested_by_customer' ,
description: `Cancelamento do pedido # ${ orderId } ` ,
notify_customer: true ,
metadata: {
order_id: orderId ,
cancelled_by: 'customer' ,
cancel_reason: reason
}
});
// Atualizar status do pedido
await updateOrderStatus ( orderId , 'cancelled' );
// Notificar equipes internas
await notifyRefundProcessed ( orderId , refund );
return { success: true , refund: refund };
} else {
// Apenas cancelar se não foi pago ainda
await updateOrderStatus ( orderId , 'cancelled' );
return { success: true , message: 'Pedido cancelado sem cobrança' };
}
} catch ( error ) {
console . error ( 'Erro ao cancelar pedido:' , error );
throw error ;
}
}
// Estorno parcial para produto com defeito em marketplace
async function refundDefectiveProduct ( marketplaceOrder , defectiveItems ) {
const refundAmount = defectiveItems . reduce (( sum , item ) => sum + item . price , 0 );
// Calcular split proporcional
const originalSplit = await getSplitDetails ( marketplaceOrder . payment_id );
const refundSplit = originalSplit . map ( split => ({
recipient_id: split . recipient_id ,
amount: Math . round (( split . amount / marketplaceOrder . total ) * refundAmount )
}));
const refund = await createRefund ({
payment_id: marketplaceOrder . payment_id ,
amount: refundAmount ,
reason: 'product_defective' ,
description: `Estorno por produto com defeito: ${ defectiveItems . map ( i => i . name ). join ( ', ' ) } ` ,
split_behavior: 'custom' ,
split_config: refundSplit ,
metadata: {
marketplace_order_id: marketplaceOrder . id ,
defective_items: defectiveItems . map ( i => i . id ),
processed_by: 'customer_service'
}
});
// Processar logística reversa
await createReturnShipping ( marketplaceOrder . id , defectiveItems );
// Notificar vendedor
await notifySellerOfRefund ( marketplaceOrder . seller_id , refund );
return refund ;
}
Assinatura - Cancelamento Proporcional
// Estorno proporcional para cancelamento de assinatura
async function cancelSubscriptionWithRefund ( subscriptionId , cancelDate ) {
const subscription = await getSubscription ( subscriptionId );
const lastPayment = await getLastSubscriptionPayment ( subscriptionId );
if ( lastPayment && lastPayment . status === 'paid' ) {
// Calcular período não utilizado
const billingPeriodStart = new Date ( subscription . current_period_start );
const billingPeriodEnd = new Date ( subscription . current_period_end );
const cancelDateTime = new Date ( cancelDate );
const totalDays = ( billingPeriodEnd - billingPeriodStart ) / ( 1000 * 60 * 60 * 24 );
const unusedDays = ( billingPeriodEnd - cancelDateTime ) / ( 1000 * 60 * 60 * 24 );
if ( unusedDays > 0 ) {
const refundAmount = Math . round (( lastPayment . amount * unusedDays ) / totalDays );
const refund = await createRefund ({
payment_id: lastPayment . id ,
amount: refundAmount ,
reason: 'requested_by_customer' ,
description: `Cancelamento de assinatura - ${ Math . round ( unusedDays ) } dias não utilizados` ,
metadata: {
subscription_id: subscriptionId ,
billing_period_start: billingPeriodStart . toISOString (),
billing_period_end: billingPeriodEnd . toISOString (),
cancel_date: cancelDate ,
unused_days: Math . round ( unusedDays )
}
});
// Atualizar assinatura
await updateSubscription ( subscriptionId , {
status: 'cancelled' ,
cancelled_at: cancelDate ,
refund_id: refund . id
});
return refund ;
}
}
// Cancelar sem estorno se não há período não utilizado
await updateSubscription ( subscriptionId , {
status: 'cancelled' ,
cancelled_at: cancelDate
});
return null ;
}
Monitoramento de Estornos
Dashboard de Métricas
// Monitorar taxa de estornos por período
class RefundMonitor {
constructor ( apiKey ) {
this . apiKey = apiKey ;
}
async getRefundMetrics ( startDate , endDate ) {
const payments = await this . getPayments ( startDate , endDate );
const refunds = await this . getRefunds ( startDate , endDate );
const metrics = {
total_payments: payments . length ,
total_refunds: refunds . length ,
refund_rate: ( refunds . length / payments . length ) * 100 ,
refund_reasons: this . groupBy ( refunds , 'reason' ),
amounts: {
total_paid: payments . reduce (( sum , p ) => sum + p . amount , 0 ),
total_refunded: refunds . reduce (( sum , r ) => sum + r . amount , 0 ),
refund_amount_rate: 0
},
timing: {
avg_refund_time: this . calculateAverageRefundTime ( refunds ),
same_day_refunds: refunds . filter ( r => this . isSameDay ( r . created_at , r . payment . created_at )). length
}
};
metrics . amounts . refund_amount_rate =
( metrics . amounts . total_refunded / metrics . amounts . total_paid ) * 100 ;
return metrics ;
}
groupBy ( array , key ) {
return array . reduce (( groups , item ) => {
const group = item [ key ];
groups [ group ] = groups [ group ] || [];
groups [ group ]. push ( item );
return groups ;
}, {});
}
// Detectar padrões anômalos
async detectAnomalies ( metrics ) {
const alerts = [];
if ( metrics . refund_rate > 15 ) {
alerts . push ({
type: 'high_refund_rate' ,
message: `Taxa de estorno alta: ${ metrics . refund_rate . toFixed ( 2 ) } %` ,
severity: 'high'
});
}
if ( metrics . refund_reasons . fraudulent ?. length > 5 ) {
alerts . push ({
type: 'fraud_pattern' ,
message: `Muitos estornos por fraude: ${ metrics . refund_reasons . fraudulent . length } ` ,
severity: 'critical'
});
}
return alerts ;
}
}
Alertas Automáticos
<? php
class RefundAlertSystem {
private $thresholds = [
'daily_refund_rate' => 10 , // % máximo por dia
'hourly_refund_count' => 20 , // máximo por hora
'suspicious_patterns' => 5 // múltiplos estornos mesmo cliente
];
public function checkRefundAlerts () {
$alerts = [];
// Verificar taxa diária
$dailyRate = $this -> getDailyRefundRate ();
if ( $dailyRate > $this -> thresholds [ 'daily_refund_rate' ]) {
$alerts [] = [
'type' => 'high_daily_refund_rate' ,
'message' => "Taxa de estorno diária alta: { $dailyRate }%" ,
'data' => [ 'rate' => $dailyRate ],
'priority' => 'high'
];
}
// Verificar padrões suspeitos
$suspiciousPatterns = $this -> detectSuspiciousPatterns ();
if ( count ( $suspiciousPatterns ) > 0 ) {
$alerts [] = [
'type' => 'suspicious_refund_patterns' ,
'message' => 'Padrões suspeitos detectados' ,
'data' => $suspiciousPatterns ,
'priority' => 'critical'
];
}
// Enviar alertas se necessário
foreach ( $alerts as $alert ) {
$this -> sendAlert ( $alert );
}
return $alerts ;
}
private function detectSuspiciousPatterns () {
// Buscar clientes com múltiplos estornos recentes
$query = "
SELECT customer_email, COUNT(*) as refund_count
FROM refunds r
JOIN payments p ON r.payment_id = p.id
WHERE r.created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY customer_email
HAVING refund_count >= ?
" ;
return $this -> db -> query ( $query , [ $this -> thresholds [ 'suspicious_patterns' ]]);
}
}
// Processar eventos de estorno via webhook
app . post ( '/webhooks/autorizou' , ( req , res ) => {
const { event , data } = req . body ;
switch ( event ) {
case 'refund.succeeded' :
handleRefundSucceeded ( data . refund );
break ;
case 'refund.failed' :
handleRefundFailed ( data . refund );
break ;
case 'refund.processing' :
updateRefundStatus ( data . refund . id , 'processing' );
break ;
}
res . status ( 200 ). send ( 'OK' );
});
async function handleRefundSucceeded ( refund ) {
// Atualizar sistema interno
await updateOrderStatus ( refund . metadata . order_id , 'refunded' );
// Notificar cliente
await sendRefundConfirmation ( refund . payment . customer . email , refund );
// Atualizar inventário se necessário
if ( refund . reason === 'product_returned' ) {
await restoreInventory ( refund . metadata . order_id );
}
// Log para análise
console . log ( `Estorno concluído: ${ refund . id } - R$ ${ refund . amount / 100 } ` );
}
Próximos Passos
Após criar um estorno:
Monitorar status via webhooks
Consultar estorno para acompanhar progresso
Configurar alertas para padrões anômalos
Analisar métricas de estorno regularmente