Guias
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
- Crie o pagamento PIX com
idempotencyKeyvinculado aoorderId - O Orquestraio retorna
status: PENDINGcom opixCode - Se o webhook
payment.approvednão chegar em 30min, ofereça cartão - Crie um segundo pagamento cartão com uma idempotencyKey diferente
- 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.