Skip to main content
POST
/
api
/
v1
/
refunds
Criar Estorno
curl --request POST \
  --url https://zeus-sandbox.autorizou.dev/api/v1/refunds \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "payment_id": "<string>",
  "amount": 123,
  "reason": "<string>",
  "description": "<string>",
  "split_behavior": "<string>",
  "split_config": [
    {
      "split_config[].recipient_id": "<string>",
      "split_config[].amount": 123
    }
  ],
  "notify_customer": true,
  "metadata": {}
}
'
{
  "id": "<string>",
  "payment_id": "<string>",
  "status": "<string>",
  "amount": 123,
  "reason": "<string>",
  "description": "<string>",
  "refund_method": "<string>",
  "estimated_arrival": "<string>",
  "split_details": [
    {
      "split_details[].recipient_id": "<string>",
      "split_details[].amount": 123,
      "split_details[].status": "<string>"
    }
  ],
  "fees": {
    "fees.processing_fee": 123,
    "fees.refund_fee": 123,
    "fees.total_refunded_to_customer": 123
  },
  "metadata": {},
  "created_at": "<string>"
}
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

payment_id
string
required
UUID da transação a ser estornadaExemplo: pay_abc123def456ghi789
amount
integer
Valor a ser estornado em centavosOmitir para estorno total ou informar valor menor para estorno parcialDeve ser ≤ valor disponível para estorno
reason
string
required
Motivo do estornoValores 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
description
string
Descrição adicional do motivo do estornoMáximo 255 caracteres - Aparece no extrato do cliente
split_behavior
string
Como tratar o split no estornoValores:
  • proportional (padrão) - Estorna proporcionalmente de cada destinatário
  • full_main - Remove tudo do destinatário principal
  • custom - Configuração customizada via split_config
split_config
array
Obrigatório quando split_behavior = "custom"
notify_customer
boolean
Se deve notificar o cliente sobre o estornoPadrão: true
metadata
object
Dados adicionais para associar ao estornoExemplo: {"order_id": "12345", "support_ticket": "TK-001"}

Exemplos de Requisição

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

id
string
UUID único do estorno
payment_id
string
UUID da transação original
status
string
Status do estornoValores: pending, processing, succeeded, failed, cancelled
amount
integer
Valor estornado em centavos
reason
string
Motivo do estorno informado
description
string
Descrição adicional do estorno
refund_method
string
Método utilizado para o estornoValores: credit_card, bank_transfer, pix
estimated_arrival
string
Data estimada para o cliente receber o estorno (ISO 8601)
split_details
array
fees
object
metadata
object
Dados adicionais fornecidos
created_at
string
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"
    }
  }
}
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
  • 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
  • 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
  • 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;
  }
}

Marketplace - Produto com Defeito

// 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']]);
    }
}

Integração com Webhooks

// 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:
  1. Monitorar status via webhooks
  2. Consultar estorno para acompanhar progresso
  3. Configurar alertas para padrões anômalos
  4. Analisar métricas de estorno regularmente