Recipes

12 min de leitura

Padrões prontos para os cenários mais comuns de integração. Copie, adapte e use em produção.

01
E-commerce — PIX como primário, cartão como fallback E-COMMERCE
Cenário: Sua loja quer oferecer PIX com desconto. Se o cliente não pagar em 30 minutos, você envia um link de cartão como alternativa. Dois pagamentos distintos, mas apenas um pode ser cobrado.

Como funciona

  1. Crie o pagamento PIX com idempotencyKey vinculado ao orderId
  2. O Orquestraio retorna status: PENDING com o pixCode
  3. Se o webhook payment.approved não chegar em 30min, ofereça cartão
  4. Crie um segundo pagamento cartão com uma idempotencyKey diferente
  5. Quando um dos dois for aprovado, cancele o outro via webhook

Passo 1 — Criar o pagamento PIX

async function createPixPayment(order) { const res = await fetch('https://api.orquestraio.com/v1/payments', { method: 'POST', headers: { 'x-api-key': process.env.OZ_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ // chave vinculada ao pedido + método idempotencyKey: `pix-${order.id}`, amount: order.total, currency: 'BRL', paymentMethodRequest: { type: 'pix' }, customer: order.customer, metadata: { orderId: order.id, description: order.description }, }), }); const payment = await res.json(); // Salve pixCode no banco para exibir ao cliente await db.orders.update(order.id, { pixPaymentId: payment.paymentId, pixCode: payment.details.pixCode, pixExpiration: payment.details.pixExpiration, }); // Agendar fallback para cartão em 30min await queue.schedule('offer-card-fallback', { orderId: order.id, pixPaymentId: payment.paymentId, }, { delay: 30 * 60 * 1000 }); return payment; }
import requests, os def create_pix_payment(order): res = requests.post( "https://api.orquestraio.com/v1/payments", headers={"x-api-key": os.environ["OZ_API_KEY"]}, json={ "idempotencyKey": f"pix-{order['id']}", "amount": order["total"], "currency": "BRL", "paymentMethodRequest": {"type": "pix"}, "customer": order["customer"], "metadata": {"orderId": order["id"]}, } ).json() # Salve no banco e agende o fallback db.orders.update(order["id"], { "pix_payment_id": res["paymentId"], "pix_code": res["details"]["pixCode"], }) queue.schedule("offer_card_fallback", order["id"], delay_minutes=30) return res

Passo 2 — Fallback para cartão (após 30min)

async function offerCardFallback({ orderId, pixPaymentId }) { const order = await db.orders.findById(orderId); // Só age se PIX ainda não foi pago if (order.status === 'paid') return; // Envia e-mail/notificação com link de pagamento via cartão await email.send(order.customer.email, { template: 'pix-expired-try-card', checkoutUrl: `https://checkout.suaapp.com/card/${orderId}`, }); } // No checkout de cartão — idempotencyKey DIFERENTE do PIX async function createCardPayment(orderId, cardToken) { return fetch('https://api.orquestraio.com/v1/payments', { method: 'POST', headers: { 'x-api-key': process.env.OZ_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ idempotencyKey: `card-${orderId}`, // diferente de "pix-{orderId}" amount: order.total, currency: 'BRL', paymentMethodRequest: { type: 'card_token', token: cardToken }, customer: order.customer, metadata: { orderId }, }), }).then(r => r.json()); }
Idempotência: Use prefixos diferentes (pix-{orderId} vs card-{orderId}) para que o Orquestraio trate como duas transações independentes. Nunca reutilize a mesma chave para métodos diferentes.
02
SaaS recorrente — cobrança mensal bulletproof SAAS
Cenário: Job cron que cobra todos os planos no dia 1º do mês. O job pode rodar duas vezes por bug, ou um worker pode reprocessar uma mensagem da fila. A cobrança deve ser feita exatamente uma vez por mês por cliente.

A chave da idempotência determinística

Gere a idempotencyKey a partir de dados imutáveis do contexto de cobrança. Assim, re-execuções do mesmo job produzem exatamente a mesma chave e o Orquestraio retorna o resultado cacheado sem cobrar de novo.

async function billSubscription(subscription) { const billingMonth = new Date().toISOString().slice(0, 7); // "2026-06" // Chave determinística: mesma sub + mesmo mês = mesma key const idempotencyKey = `sub-${subscription.id}-${billingMonth}`; const res = await fetch('https://api.orquestraio.com/v1/payments', { method: 'POST', headers: { 'x-api-key': process.env.OZ_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ idempotencyKey, amount: subscription.plan.price, currency: 'BRL', paymentMethodRequest: { type: 'card_token', token: subscription.savedCardToken, }, customer: subscription.customer, metadata: { orderId: `invoice-${subscription.id}-${billingMonth}`, description: `${subscription.plan.name} — ${billingMonth}`, }, }), }); const payment = await res.json(); // 409 = já cobrado este mês. Trate como sucesso. if (res.status === 409) { console.log(`Sub ${subscription.id} já cobrada em ${billingMonth} — idempotência OK`); return { alreadyBilled: true }; } if (payment.status === 'DECLINED') { await handleDecline(subscription, payment.details.declineCode); } return payment; }
import requests, os from datetime import datetime def bill_subscription(subscription): billing_month = datetime.now().strftime("%Y-%m") # "2026-06" # Chave determinística idempotency_key = f"sub-{subscription['id']}-{billing_month}" r = requests.post( "https://api.orquestraio.com/v1/payments", headers={"x-api-key": os.environ["OZ_API_KEY"]}, json={ "idempotencyKey": idempotency_key, "amount": subscription["plan"]["price"], "currency": "BRL", "paymentMethodRequest": { "type": "card_token", "token": subscription["saved_card_token"], }, "customer": subscription["customer"], "metadata": { "orderId": f"invoice-{subscription['id']}-{billing_month}", }, } ) # 409 = já cobrado este mês — trate como sucesso if r.status_code == 409: return {"already_billed": True} return r.json()
HTTP 409 é seu amigo: Quando você recebe 409 com IDEMPOTENCY_CONFLICT usando a mesma chave e mesmo payload, significa que o pagamento já existe. Logue e siga em frente — não é erro.
03
Retry manual com gateway diferente RESILIÊNCIA
Cenário: Você força um gateway específico mas ele recusa o pagamento por razão técnica (timeout, instabilidade). Você quer tentar o mesmo pagamento em outro gateway automaticamente, sem cobrar duas vezes.
Prefira o SmartRouter: Se você omitir metadata.gateway, o Orquestraio já faz esse fallback automaticamente com Circuit Breaker. Use este recipe apenas quando precisar forçar a ordem dos gateways manualmente.
const GATEWAY_PRIORITY = ['STRIPE', 'MERCADO_PAGO', 'PAYPAL']; // Códigos que indicam problema no gateway (não no cartão) const GATEWAY_ERRORS = ['gateway_timeout', 'processing_error', 'gateway_unavailable']; async function payWithFallback(orderData) { const baseKey = orderData.idempotencyKey; for (const [i, gateway] of GATEWAY_PRIORITY.entries()) { const res = await fetch('https://api.orquestraio.com/v1/payments', { method: 'POST', headers: { 'x-api-key': process.env.OZ_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ // Sufixo do gateway garante chave única por tentativa idempotencyKey: `${baseKey}-${gateway.toLowerCase()}`, ...orderData, metadata: { ...orderData.metadata, gateway }, }), }); const payment = await res.json(); if (payment.status === 'APPROVED') return payment; // Recusa por problema no gateway → tentar o próximo const isGatewayError = GATEWAY_ERRORS.includes(payment.details?.declineCode); if (!isGatewayError) { // Recusa por dados do cliente (saldo, fraude) → não adianta tentar outro return payment; } console.warn(`${gateway} falhou com ${payment.details.declineCode}, tentando próximo...`); } throw new Error('Todos os gateways falharam'); }
import requests, os GATEWAY_PRIORITY = ["STRIPE", "MERCADO_PAGO", "PAYPAL"] GATEWAY_ERRORS = {"gateway_timeout", "processing_error", "gateway_unavailable"} def pay_with_fallback(order_data): base_key = order_data["idempotencyKey"] for gateway in GATEWAY_PRIORITY: payload = { **order_data, "idempotencyKey": f"{base_key}-{gateway.lower()}", "metadata": {**order_data.get("metadata", {}), "gateway": gateway}, } r = requests.post( "https://api.orquestraio.com/v1/payments", headers={"x-api-key": os.environ["OZ_API_KEY"]}, json=payload, ).json() if r["status"] == "APPROVED": return r decline_code = r.get("details", {}).get("declineCode", "") if decline_code not in GATEWAY_ERRORS: return r # erro do cliente, não do gateway raise Exception("Todos os gateways falharam")
04
Webhook confiável — processar sem duplicação WEBHOOKS
Cenário: O Orquestraio (e os gateways upstream) reenviam webhooks em caso de falha de entrega. Seu endpoint pode receber o mesmo evento 2–3 vezes. A lógica de negócio (liberar acesso, enviar NF) deve rodar exatamente uma vez.
app.post('/webhooks/orquestraio', async (req, res) => { // 1. Validar assinatura HMAC antes de qualquer coisa const signature = req.headers['x-oz-signature']; if (!validateHmac(req.rawBody, signature, process.env.OZ_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = req.body; // 2. Checar idempotência: já processamos este evento? const alreadyProcessed = await db.webhookEvents.exists({ eventId: event.eventId, }); if (alreadyProcessed) { // Retornar 200 sempre — o Orquestraio vai parar de reenviar return res.status(200).json({ status: 'already_processed' }); } // 3. Marcar como processado ANTES de executar lógica await db.webhookEvents.insert({ eventId: event.eventId, receivedAt: new Date() }); // 4. Processar o evento if (event.type === 'payment.approved') { await fulfillOrder(event.paymentId, event.metadata?.orderId); } else if (event.type === 'payment.declined') { await notifyCustomerDecline(event.metadata?.orderId); } // 5. Sempre responder 200 para o Orquestraio parar de reenviar res.status(200).json({ received: true }); });
from fastapi import Request, HTTPException import hmac, hashlib, os @app.post("/webhooks/orquestraio") async def handle_webhook(request: Request): raw_body = await request.body() signature = request.headers.get("x-oz-signature", "") # 1. Validar assinatura HMAC expected = hmac.new( os.environ["OZ_WEBHOOK_SECRET"].encode(), raw_body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, signature): raise HTTPException(status_code=401) event = await request.json() # 2. Idempotência if db.webhook_events.exists(event["eventId"]): return {"status": "already_processed"} # 3. Registrar ANTES de agir db.webhook_events.insert(event["eventId"]) # 4. Processar if event["type"] == "payment.approved": fulfill_order(event["paymentId"]) elif event["type"] == "payment.declined": notify_decline(event.get("metadata", {}).get("orderId")) # 5. Sempre 200 return {"received": True}
Grave antes de agir: Registre o eventId no banco antes de executar a lógica de negócio. Se você agir primeiro e a gravação falhar, você pode executar duas vezes. Ver Webhooks para o formato completo do evento.