Inyo

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

EventoDescrição
TransactionStatusChangedA transação avançou na máquina de estados interna da Inyo (ex.: PaymentAuthorizedProcessingPayout). O evento mais frequente — uma transação típica produz de 8 a 10 destes ao longo do seu ciclo de vida.
TransactionComplianceStatusChangedEventO status de conformidade da transação mudou (ex.: PendingApproved). Dispara apenas quando o valor de conformidade realmente difere do valor anterior.
TransactionPayoutStatusChangedO 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.
DocumentUpdatedEventsUm documento foi enviado, ou sua verificação foi concluída (aprovado/rejeitado).
AgentUpdatedEventsUm registro de agente foi criado.

Capitalização do nome do evento: a correspondência de assinaturas é case-insensitive — TransactionStatusChanged, transactionstatuschanged e TRANSACTIONSTATUSCHANGED assinam o mesmo evento. O campo event no 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 Event no 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"
  ]
}'
CampoTipoObrigatórioDescrição
urlstringSimEndpoint HTTPS para receber as entregas (máx. 2048 caracteres)
eventsstring[]SimPelo 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"
}
CampoDescrição
signingSecretO 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.
secretFingerprintPrimeiros 8 caracteres hex de sha256(secret) — seguro para exibição; use-o para confirmar qual segredo está ativo
signatureVersionVersão do esquema de assinatura, atualmente v1
secretRotatedAtQuando 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"]
}
CampoTipoNotas
eventstringEcoa a grafia que você registrou
transactionIduuidUUID da transação na Inyo
externalTransactionIdstring | nullSeu externalId da criação da transação; null se você não enviou um
tenantIdstringO slug do seu tenant
oldStatusstring | nullStatus anterior; null na primeira transição
newStatusstringNovo status — veja os valores abaixo
newStatusMessagesstring[]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": []
}
CampoTipoNotas
oldComplianceStatusstring | nullEm Title Case; null na primeira transição
newComplianceStatusstringEm 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"]
}
CampoTipoNotas
oldPayoutStatusstring | nullStatus bruto do gateway
newPayoutStatusstringStatus bruto do gateway — veja abaixo
gatewayPaymentIdstring | nullA referência de pagamento própria do gateway
gmtReceiptstring | nullPreenchido quando o payout foi roteado pela rede MSB (a maioria dos casos)
amountobject | 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úsculas documentUpdatedEvents continuam 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
}
CampoTipoNotas
iduuidO UUID do envio do documento
entityIduuidO participante ao qual o documento pertence
entityTypestringParticipant
documentTypestringPASSPORT, DRIVER_LICENSE, SSN, PROOF_OF_FUNDS, etc. — mesma taxonomia dos endpoints de envio
verificationStatusstringPENDING (enviado, aguardando verificação), VERIFIED ou REJECTED. Compare de forma case-insensitive — o evento inicial de envio pode entregar Pending.
versionnumberVersã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úsculas agentUpdatedEvents continuam 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
ComponenteSignificado
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 componente t=. 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

  1. Leia o header X-Inyo-Signature. Rejeite se ausente.
  2. Extraia t e o componente v1. Rejeite se qualquer um estiver ausente ou malformado.
  3. Rejeite se |now − t| > 300 segundos (janela de replay de 5 minutos; ajuste para a sua tolerância de desvio de relógio).
  4. Monte signed_string = t + "." + raw_body.
  5. Calcule expected = HMAC-SHA256(secret, signed_string), codificado em hex minúsculo.
  6. Compare expected com o valor v1 em tempo constante (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) — nunca ==.
  7. 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:

  1. Aceite temporariamente entregas que falham na verificação (registre-as em log em vez de rejeitá-las).
  2. Chame POST /organizations/{tenant}/webhooks/{webhookId}/secret e armazene o novo signingSecret da resposta.
  3. 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 2xx imediatamente e processe de forma assíncrona — handlers demorados são interrompidos e retentados, produzindo duplicatas.
  • Assinaturas: as entregas carregam um header X-Inyo-Signature HMAC-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 via GET /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 oldStatus recebido 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çãoMétodoEndpoint
Registrar webhookPOST/organizations/{tenant}/webhooks
Listar webhooksGET/organizations/{tenant}/webhooks
Excluir webhookDELETE/organizations/{tenant}/webhooks/{webhookId}
Gerar / rotacionar segredo de assinaturaPOST/organizations/{tenant}/webhooks/{webhookId}/secret
Desabilitar assinaturaDELETE/organizations/{tenant}/webhooks/{webhookId}/secret