Webhooks
Webhooks permitem receber notificações em tempo real quando eventos importantes ocorrem na plataforma Inyo — como uma transação mudando de status, uma decisão de conformidade ou a conclusão da verificação de um documento. Em vez de fazer polling na API, você registra uma URL de callback e a Inyo envia os eventos para você.
Eventos Suportados
| Evento | Descrição |
|---|---|
TransactionStatusChanged | A transação avançou na máquina de estados interna da Inyo (ex.: PaymentAuthorized → ProcessingPayout). O evento mais frequente — uma transação típica produz de 8 a 10 destes ao longo do seu ciclo de vida. |
TransactionComplianceStatusChangedEvent | O status de conformidade da transação mudou (ex.: Pending → Approved). Dispara apenas quando o valor de conformidade realmente difere do valor anterior. |
TransactionPayoutStatusChanged | O gateway de pagamento reportou uma mudança de status na perna de payout (Inyo → destinatário). Sinal do gateway quase em tempo real, mais rápido que o evento da máquina de estados. |
DocumentUpdatedEvents | Um documento foi enviado, ou sua verificação foi concluída (aprovado/rejeitado). |
AgentUpdatedEvents | Um registro de agente foi criado. |
Capitalização do nome do evento: a correspondência de assinaturas é case-insensitive —
TransactionStatusChanged,transactionstatuschangedeTRANSACTIONSTATUSCHANGEDassinam o mesmo evento. O campoeventno payload entregue ecoa a grafia exata que você registrou, portanto compare-o de forma case-insensitive no seu handler.
O nome do evento de conformidade carrega um sufixo
Eventno final (TransactionComplianceStatusChangedEvent) — histórico, mantido por compatibilidade retroativa.
Registrando um Webhook
Endpoint: POST /organizations/{tenant}/webhooks
Autenticação: Nível de tenant (x-api-key)
curl --request POST \
--url https://{FQDN}/organizations/$TENANT/webhooks \
--header 'Content-Type: application/json' \
--header "x-api-key: $API_KEY" \
--data '{
"url": "https://your-server.com/api/webhooks/inyo",
"events": [
"TransactionStatusChanged",
"TransactionComplianceStatusChangedEvent",
"TransactionPayoutStatusChanged",
"DocumentUpdatedEvents",
"AgentUpdatedEvents"
]
}'
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
url | string | Sim | Endpoint HTTPS para receber as entregas (máx. 2048 caracteres) |
events | string[] | Sim | Pelo menos um nome de evento (correspondência case-insensitive) |
Regras de assinatura de eventos:
- Apenas os eventos que você lista disparam naquele endpoint.
- Múltiplos endpoints por tenant são suportados — registre uma vez por URL, cada uma com sua própria lista de eventos.
- Não há endpoint de atualização. Para alterar a lista de eventos de uma assinatura existente, exclua-a e recrie-a.
As entregas são não assinadas por padrão. Para que toda entrega carregue um header X-Inyo-Signature HMAC-SHA256, gere um segredo de assinatura para a assinatura — veja a próxima seção.
Gerando ou Rotacionando um Segredo de Assinatura
Endpoint: POST /organizations/{tenant}/webhooks/{webhookId}/secret
Autenticação: Nível de agente (x-api-key + x-agent-id + x-agent-api-key)
Gera um segredo de assinatura na primeira chamada, ou o rotaciona (substituindo o atual atomicamente) nas chamadas subsequentes. Uma vez definido um segredo, toda entrega para aquela assinatura carrega o header X-Inyo-Signature.
Ao contrário dos outros endpoints de webhook, as operações de segredo exigem credenciais de agente além da chave do tenant. Rotacionar um segredo de assinatura é sensível do ponto de vista de segurança (concede a capacidade de forjar ou verificar entregas assinadas), então uma identidade de agente é exigida para a trilha de auditoria — o mesmo rigor dos endpoints transacionais.
curl --request POST \
--url https://{FQDN}/organizations/$TENANT/webhooks/$WEBHOOK_ID/secret \
--header "x-api-key: $API_KEY" \
--header "x-agent-id: $AGENT_ID" \
--header "x-agent-api-key: $AGENT_KEY"
Resposta (200):
{
"signingSecret": "nZl3F0p9pKcW1sT7Yb2eXo4qUvJ8hRmA6dCgE5wLiSk=",
"secretFingerprint": "abcd1234",
"signatureVersion": "v1",
"secretRotatedAt": "2026-08-04T15:30:00+00:00"
}
| Campo | Descrição |
|---|---|
signingSecret | O segredo bruto (44 caracteres em base64 de 32 bytes aleatórios). Exibido apenas nesta resposta — armazene-o imediatamente no seu gerenciador de segredos. Não há endpoint GET; o valor nunca pode ser recuperado novamente. |
secretFingerprint | Primeiros 8 caracteres hex de sha256(secret) — seguro para exibição; use-o para confirmar qual segredo está ativo |
signatureVersion | Versão do esquema de assinatura, atualmente v1 |
secretRotatedAt | Quando o segredo foi gerado/rotacionado |
A rotação substitui o segredo imediatamente — não há período de carência com dois segredos. Entregas assinadas com o segredo antigo param no momento em que você rotaciona; veja Rotação de Segredo para um procedimento sem downtime.
Desabilitando a Assinatura
Endpoint: DELETE /organizations/{tenant}/webhooks/{webhookId}/secret
Autenticação: Nível de agente (x-api-key + x-agent-id + x-agent-api-key)
curl --request DELETE \
--url https://{FQDN}/organizations/$TENANT/webhooks/$WEBHOOK_ID/secret \
--header "x-api-key: $API_KEY" \
--header "x-agent-id: $AGENT_ID" \
--header "x-agent-api-key: $AGENT_KEY"
Retorna 204. As entregas voltam a ser feitas sem o header X-Inyo-Signature — desabilite a verificação do seu lado primeiro, ou o seu endpoint começará a rejeitá-las.
Listando Webhooks Registrados
Endpoint: GET /organizations/{tenant}/webhooks
Autenticação: Nível de tenant (x-api-key)
curl --request GET \
--url https://{FQDN}/organizations/$TENANT/webhooks \
--header "x-api-key: $API_KEY"
Resposta:
{
"webhooks": [
{
"id": "6f0e3a3a-6f27-4a2e-9c1f-1f2f3a4b5c6d",
"url": "https://your-server.com/api/webhooks/inyo",
"events": ["TransactionStatusChanged"],
"isBatch": false,
"createdAt": "2026-08-01T12:00:00+00:00",
"signatureVersion": "v1",
"secretFingerprint": "abcd1234",
"secretRotatedAt": "2026-08-04T15:30:00+00:00"
}
]
}
O segredo de assinatura bruto nunca é incluído — secretFingerprint (null quando nenhum segredo está configurado) informa se a assinatura está ativa e qual segredo está em uso.
Excluindo um Webhook
Endpoint: DELETE /organizations/{tenant}/webhooks/{webhookId}
Autenticação: Nível de tenant (x-api-key)
curl --request DELETE \
--url https://{FQDN}/organizations/$TENANT/webhooks/$WEBHOOK_ID \
--header "x-api-key: $API_KEY"
Payloads dos Eventos
Todas as entregas são requisições POST para a sua URL registrada com Content-Type: application/json.
1. TransactionStatusChanged
Dispara quando a máquina de estados interna da Inyo transiciona a transação.
{
"event": "TransactionStatusChanged",
"transactionId": "0a3ca5d5-927d-4784-9dbe-a10087a746cc",
"externalTransactionId": "de70d60f-11e7-431c-bd5d-4ae4f2f808e4",
"tenantId": "your-tenant-slug",
"oldStatus": "Paid",
"newStatus": "Completed",
"newStatusMessages": ["Transaction completed"]
}
| Campo | Tipo | Notas |
|---|---|---|
event | string | Ecoa a grafia que você registrou |
transactionId | uuid | UUID da transação na Inyo |
externalTransactionId | string | null | Seu externalId da criação da transação; null se você não enviou um |
tenantId | string | O slug do seu tenant |
oldStatus | string | null | Status anterior; null na primeira transição |
newStatus | string | Novo status — veja os valores abaixo |
newStatusMessages | string[] | Contexto legível por humanos, se houver |
Valores de status:
Created, PaymentProcessing, WaitingChallenge3ds, PaymentAuthorized, PaymentDeclined, PaymentCaptured, WaitingSettlement, PaymentSettled, ProcessingPayout, PayoutAccepted, PayoutHold, PayoutReleased, PayoutRejected, ManualReview, ReviewApproved, ReviewRejected, WaitingPayout, Paid, Completed, Cancelled, CancelRequested, Refunded, Voided, Error, PendingReversalApproval, BlockedPendingReview.
Sequência típica do caminho feliz (cartão, sem 3DS):
Created → PaymentProcessing → PaymentAuthorized → ProcessingPayout
→ PayoutAccepted → ReviewApproved → PaymentCaptured
→ WaitingPayout → Paid → Completed
Veja o ciclo de vida da transação para os fluxos de estado completos, incluindo 3DS e ACH.
2. TransactionComplianceStatusChangedEvent
Dispara apenas quando o status de conformidade realmente muda. Transições de estado que não movem a conformidade não emitem este evento.
{
"event": "TransactionComplianceStatusChangedEvent",
"transactionId": "0a3ca5d5-927d-4784-9dbe-a10087a746cc",
"externalTransactionId": "de70d60f-11e7-431c-bd5d-4ae4f2f808e4",
"tenantId": "your-tenant-slug",
"oldComplianceStatus": "Pending",
"newComplianceStatus": "Approved",
"newStatusMessages": []
}
| Campo | Tipo | Notas |
|---|---|---|
oldComplianceStatus | string | null | Em Title Case; null na primeira transição |
newComplianceStatus | string | Em Title Case — veja os valores abaixo |
Valores de status de conformidade: Pending, Approved, Rejected, Cancelled, Refunded, Failed.
A maioria das transações produz um único evento de conformidade: Pending → Approved (ou uma transição terminal como Pending → Cancelled). Observe que uma retenção de payout na GMT não altera o status de conformidade — retenções aparecem como PayoutHold em TransactionStatusChanged.
3. TransactionPayoutStatusChanged
Dispara quando o gateway de pagamento reporta uma mudança de status na perna de payout de uma transação (Inyo → banco/carteira do destinatário). Este é um sinal do gateway quase em tempo real, distinto de TransactionStatusChanged — este último reflete a máquina de estados interna da Inyo, que no lado do payout é movida principalmente por um poller periódico. Assine se quiser visibilidade mais rápida do payout.
{
"event": "TransactionPayoutStatusChanged",
"transactionId": "0a3ca5d5-927d-4784-9dbe-a10087a746cc",
"externalTransactionId": "de70d60f-11e7-431c-bd5d-4ae4f2f808e4",
"tenantId": "your-tenant-slug",
"oldPayoutStatus": "PENDING",
"newPayoutStatus": "AUTHORIZED",
"gatewayPaymentId": "d5af3652-909e-44b6-9f89-f14d055baf26",
"gmtReceipt": "GMT000641475307",
"amount": {
"total": 48.74999979043565,
"currency": "USD"
},
"newStatusMessages": ["Payment: CONFIRM_PENDING"]
}
| Campo | Tipo | Notas |
|---|---|---|
oldPayoutStatus | string | null | Status bruto do gateway |
newPayoutStatus | string | Status bruto do gateway — veja abaixo |
gatewayPaymentId | string | null | A referência de pagamento própria do gateway |
gmtReceipt | string | null | Preenchido quando o payout foi roteado pela rede MSB (a maioria dos casos) |
amount | object | null | { "total": number, "currency": string }. total é um float bruto — não arredondado para as unidades menores da moeda. Arredonde do seu lado. |
newPayoutStatus é a string bruta que o gateway enviou — sem mapeamento de enum do lado da Inyo. Valores comuns: AUTHORIZED, PENDING, CAPTURED, DECLINED, VOIDED, REFUNDED, ERROR. Novos códigos do gateway podem aparecer a qualquer momento — trate strings desconhecidas como "nenhuma ação necessária" em vez de gerar erro.
4. DocumentUpdatedEvents
Dispara quando um documento é enviado, e novamente quando a verificação é concluída (OCR ou análise manual).
Assine como
DocumentUpdatedEvents(recomendado). Assinaturas existentes registradas com a grafia legada em minúsculasdocumentUpdatedEventscontinuam funcionando e continuam recebendo essa grafia no payload.
{
"event": "DocumentUpdatedEvents",
"id": "4ec66735-216b-4ab4-b1d7-00558baa6d85",
"entityId": "6588c7e7-3b1a-42ff-94ab-a834eda66640",
"entityType": "Participant",
"documentType": "DRIVER_LICENSE",
"verificationStatus": "VERIFIED",
"tenantId": "your-tenant-slug",
"version": 1
}
| Campo | Tipo | Notas |
|---|---|---|
id | uuid | O UUID do envio do documento |
entityId | uuid | O participante ao qual o documento pertence |
entityType | string | Participant |
documentType | string | PASSPORT, DRIVER_LICENSE, SSN, PROOF_OF_FUNDS, etc. — mesma taxonomia dos endpoints de envio |
verificationStatus | string | PENDING (enviado, aguardando verificação), VERIFIED ou REJECTED. Compare de forma case-insensitive — o evento inicial de envio pode entregar Pending. |
version | number | Versão do schema — sempre 1 atualmente |
Ciclo de vida típico: envio → webhook com PENDING → verificação assíncrona (minutos) → webhook com VERIFIED ou REJECTED. Se o OCR com IA não estiver habilitado para o seu tenant, apenas o webhook de pendência dispara e a verificação é feita manualmente pela equipe de conformidade.
5. AgentUpdatedEvents
Dispara quando um registro de agente é criado via POST /organizations/{tenant}/agents.
Assine como
AgentUpdatedEvents(recomendado). Assinaturas existentes registradas com a grafia legada em minúsculasagentUpdatedEventscontinuam funcionando e continuam recebendo essa grafia no payload.
{
"event": "AgentUpdatedEvents",
"id": "88fa9606-8345-454a-9669-19f13ccdae13",
"before": null,
"after": {
"id": "88fa9606-8345-454a-9669-19f13ccdae13",
"email": "[email protected]",
"businessName": "Your Agent Business Name",
"externalId": null,
"status": "PENDING_APPROVAL",
"createdAt": "2026-06-02T18:20:10+00:00"
},
"tenantId": "your-tenant-slug",
"version": 1
}
before é null na criação (o único caminho que atualmente emite este evento).
Verificando Assinaturas (HMAC-SHA256)
Quando um segredo de assinatura está configurado para a sua assinatura, toda entrega carrega um header X-Inyo-Signature. Verificá-lo permite que seu endpoint rejeite requisições forjadas e replays.
Formato do Header
X-Inyo-Signature: t=1704829200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
| Componente | Significado |
|---|---|
t=<seconds> | Timestamp Unix (segundos) em que a assinatura foi calculada — use-o para rejeitar replays fora de uma janela de tolerância |
v1=<hex> | HMAC-SHA256 codificado em hex. v1 é a versão atual do esquema; revisões futuras usarão v2, v3, … para que você possa migrar sem um corte abrupto |
Se a sua assinatura não tiver segredo de assinatura, o header não é enviado e a entrega é não assinada.
A String de Bytes Assinada
A assinatura é HMAC-SHA256(secret, signed_string), onde signed_string é a concatenação ASCII:
<timestamp> + "." + <raw request body>
<timestamp>— os dígitos ASCII exatos do componentet=. Use a substring como está; não re-analise e reformate."."— um único ponto ASCII, sem espaços em branco.<raw request body>— os bytes exatos do corpo da requisição HTTP, antes de qualquer framework tê-lo analisado ou re-serializado.
Assine sobre os bytes brutos — este é o passo que os integradores mais erram. Não faça
JSON.parse()e re-stringify()do corpo: isso reordena as chaves e re-escapa caracteres, quebrando a assinatura. Capture o corpo bruto antes de o middleware JSON do seu framework rodar — Express:express.raw({ type: 'application/json' }); Django:request.body; Rails:request.raw_post; Laravel:$request->getContent().
A Inyo serializa as entregas de forma compacta (sem pretty-printing), com barras e caracteres não ASCII sem escape, e sem newline final — mas nada disso importa se você verificar contra os bytes brutos conforme recebidos.
Passos de Verificação
- Leia o header
X-Inyo-Signature. Rejeite se ausente. - Extraia
te o componentev1. Rejeite se qualquer um estiver ausente ou malformado. - Rejeite se
|now − t| > 300segundos (janela de replay de 5 minutos; ajuste para a sua tolerância de desvio de relógio). - Monte
signed_string = t + "." + raw_body. - Calcule
expected = HMAC-SHA256(secret, signed_string), codificado em hex minúsculo. - Compare
expectedcom o valorv1em tempo constante (crypto.timingSafeEqual,hmac.compare_digest,hash_equals) — nunca==. - Somente depois que todas as checagens passarem, faça o parse JSON do corpo e aja sobre o payload.
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
// IMPORTANTE: corpo bruto, não JSON parseado. Registre ANTES de qualquer middleware express.json().
app.post('/webhooks/inyo',
express.raw({ type: 'application/json' }),
(req, res) => {
const secret = process.env.INYO_WEBHOOK_SECRET;
const header = req.get('X-Inyo-Signature') || '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const { t, v1: sig } = parts;
if (!t || !sig) return res.status(400).send('bad signature header');
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(400).send('stale');
const signedString = t + '.' + req.body.toString('utf8');
const expected = crypto.createHmac('sha256', secret).update(signedString).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sig, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(400).send('signature mismatch');
}
// Confirme imediatamente — você tem 10 segundos antes de a entrega expirar
res.status(200).send('OK');
const { event, transactionId, oldStatus, newStatus } = JSON.parse(req.body.toString('utf8'));
if (event.toLowerCase() === 'transactionstatuschanged') {
// …atualize seus registros…
}
});
app.listen(3001);
Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["INYO_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/inyo")
def inyo_webhook():
header = request.headers.get("X-Inyo-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, sig = parts.get("t"), parts.get("v1")
if not t or not sig:
abort(400)
if abs(int(time.time()) - int(t)) > 300:
abort(400)
raw = request.get_data() # bytes, não request.json
signed = t.encode() + b"." + raw
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(400)
payload = request.get_json()
# …processe o payload…
return "", 200
Rotação de Segredo
Um segredo fica ativo por assinatura; a rotação o substitui imediatamente — não há período de carência com dois segredos. Para rotacionar sem downtime:
- Aceite temporariamente entregas que falham na verificação (registre-as em log em vez de rejeitá-las).
- Chame
POST /organizations/{tenant}/webhooks/{webhookId}/secrete armazene o novosigningSecretda resposta. - Reabilite a verificação estrita com o novo segredo.
Confirme qual segredo está ativo a qualquer momento comparando o secretFingerprint do GET /webhooks com os primeiros 8 caracteres hex de sha256(your_stored_secret).
Modos de Falha Comuns
- Corpo re-serializado antes de verificar — o middleware fez o parse do JSON e o seu handler o re-stringificou. Capture os bytes brutos antes de qualquer parser.
- Desvio de encoding — ler o corpo como latin-1 e re-codificar como UTF-8 altera os bytes. Mantenha os bytes como estão até depois da verificação.
- Comparação em tempo não constante —
==abre um canal lateral de timing; use a comparação em tempo constante da sua linguagem. - Desvio de relógio — um relógio de host com desvio superior a 5 minutos rejeita tudo. Mantenha o NTP habilitado.
Entrega e Confiabilidade
- Retentativas: respostas não-2xx (e timeouts) são retentadas até 3 tentativas no total, espaçadas cerca de 1 minuto.
- Timeout: seu endpoint tem 10 segundos para responder. Retorne um
2xximediatamente e processe de forma assíncrona — handlers demorados são interrompidos e retentados, produzindo duplicatas. - Assinaturas: as entregas carregam um header
X-Inyo-SignatureHMAC-SHA256 quando um segredo de assinatura está configurado para a sua assinatura (veja Verificando Assinaturas); caso contrário, são não assinadas. Especialmente para assinaturas não assinadas, trate o payload como uma dica: busque o estado oficial viaGET /fx/transactions/{id}antes de tomar ações críticas de negócio. - Duplicatas: retentativas podem entregar o mesmo evento mais de uma vez (não há header de delivery-id). Chaveie seu handler em
(transactionId, newStatus)(ou uma tupla equivalente) e trate repetições como no-ops. - Ordenação: não garantida. Dois eventos da mesma transação podem chegar fora de ordem. Se a sequência importar, compare o
oldStatusrecebido com o estado que você tem armazenado — se não corresponderem, ignore a entrega e deixe uma entrega posterior (ou um poll) reconciliar.
Todos os Endpoints
| Operação | Método | Endpoint |
|---|---|---|
| Registrar webhook | POST | /organizations/{tenant}/webhooks |
| Listar webhooks | GET | /organizations/{tenant}/webhooks |
| Excluir webhook | DELETE | /organizations/{tenant}/webhooks/{webhookId} |
| Gerar / rotacionar segredo de assinatura | POST | /organizations/{tenant}/webhooks/{webhookId}/secret |
| Desabilitar assinatura | DELETE | /organizations/{tenant}/webhooks/{webhookId}/secret |
