> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nyxpag.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Eventos de transação: transaction.approved, failed e cancelled

> Formato dos webhooks transaction.approved, transaction.failed e transaction.cancelled para cobranças e transferências Pix, com payload completo e como tratar cada um.

Quando uma cobrança ou uma transferência sai de `pending`, a NyxPag envia um evento `transaction.*` para a sua URL. É o jeito certo de saber que um Pix foi pago, sem ficar consultando.

## Os três eventos

| Evento | Quando chega | O que você faz |
| - | - | - |
| `transaction.approved` | O Pix foi pago (cobrança) ou enviado (transferência) | Libere o pedido, dê baixa, concilie |
| `transaction.failed` | A operação falhou de forma definitiva | Avise o cliente ou tente uma **nova** operação |
| `transaction.cancelled` | A cobrança foi cancelada, inclusive por expirar | Encerre o pedido ou gere uma nova cobrança |

<Note>
  Uma cobrança Pix vale **15 minutos** a partir da criação. Se não for paga nesse prazo, a NyxPag a cancela e você recebe `transaction.cancelled` com `data.expired: true`.
</Note>

Os eventos valem para os dois sentidos: cobranças (`direction: "in"`, `type: "payment"`) e transferências (`direction: "out"`, `type: "withdrawal"`). Confira `data.type` para saber qual é.

## Formato do corpo

```json theme={"dark"}
{
  "id": "evt_txn_mabc123_0123456789abcdef_approved",
  "event": "transaction.approved",
  "createdAt": "2026-10-03T14:01:32.000Z",
  "data": { "...": "a transação completa, igual ao GET /transactions/{id}" }
}
```

| Campo | Tipo | Descrição |
| - | - | - |
| `id` | string | ID do evento: `evt_<id da transação>_<status>`. É estável, então serve para deduplicar |
| `event` | string | Nome do evento. Igual ao cabeçalho `X-NyxPag-Event` |
| `createdAt` | string | Quando o evento foi gerado, em ISO 8601 UTC |
| `data` | object | A [transação](/guides/confiabilidade/ciclo-de-vida) no estado atual |

## Exemplo: cobrança aprovada

```json transaction.approved theme={"dark"}
{
  "id": "evt_txn_mabc123_0123456789abcdef_approved",
  "event": "transaction.approved",
  "createdAt": "2026-10-03T14:01:32.000Z",
  "data": {
    "id": "txn_mabc123_0123456789abcdef",
    "externalId": "PEDIDO-1042",
    "acquirer": "nyxpag",
    "direction": "in",
    "type": "payment",
    "method": "pix",
    "status": "approved",
    "expired": false,
    "processingState": "completed",
    "amount": 149.9,
    "fee": 0.5,
    "netAmount": 149.4,
    "debitedAmount": null,
    "recipientAmount": null,
    "description": "Assinatura mensal",
    "splitAmount": 0,
    "split": [],
    "payer": {
      "name": "Maria Silva",
      "document": "52998224725",
      "email": "maria@example.com",
      "bankName": "Banco Exemplo",
      "state": "SP"
    },
    "counterparty": null,
    "pix": {
      "copyPaste": "00020126...6304ABCD",
      "qrCodeBase64": "iVBORw0KGgoAAAANSUhEUgAA..."
    },
    "expiresAt": "2026-10-03T14:15:00.000Z",
    "approvedAt": "2026-10-03T14:01:31.000Z",
    "createdAt": "2026-10-03T14:00:00.000Z",
    "updatedAt": "2026-10-03T14:01:31.000Z"
  }
}
```

<Note>
  Os valores deste exemplo são ilustrativos. A tarifa real depende da sua conta e vem no campo `fee`. O objeto `payer` pode trazer dados completados pela instituição do pagador, como `bankName` e `state`, quando disponíveis. Trate CPF e e-mail como dados pessoais: não grave o corpo do webhook em logs abertos.
</Note>

## Exemplo: cobrança que expirou

O exemplo abaixo está **abreviado** para destacar o que muda: `status`, `expired` e `approvedAt`. O evento real traz todos os campos do objeto transação.

```json transaction.cancelled theme={"dark"}
{
  "id": "evt_txn_mabc123_0123456789abcdef_cancelled",
  "event": "transaction.cancelled",
  "createdAt": "2026-10-03T14:15:04.000Z",
  "data": {
    "id": "txn_mabc123_0123456789abcdef",
    "externalId": "PEDIDO-1042",
    "direction": "in",
    "type": "payment",
    "method": "pix",
    "status": "cancelled",
    "expired": true,
    "processingState": "completed",
    "amount": 149.9,
    "expiresAt": "2026-10-03T14:15:00.000Z",
    "approvedAt": null
  }
}
```

## Como tratar cada evento

<Steps>
  <Step title="Valide a assinatura">
    Antes de olhar o conteúdo. Veja [Validando a assinatura](/webhooks/assinatura).
  </Step>

  <Step title="Deduplique">
    A mesma entrega pode chegar mais de uma vez. Guarde o `id` do evento (ou o `X-NyxPag-Delivery`) e ignore repetições.
  </Step>

  <Step title="Encontre o pedido">
    Use `data.externalId`, que é o identificador que você mandou ao criar a operação. Se preferir, use `data.id`.
  </Step>

  <Step title="Aplique a regra de negócio">
    `approved` libera. `failed` e `cancelled` encerram. Trate cada um uma vez só.
  </Step>

  <Step title="Responda 2xx">
    Grave primeiro, responda rápido, processe depois. Veja [Entrega e retentativas](/guides/webhooks#entrega-e-retentativas).
  </Step>
</Steps>

<Warning>
  Os eventos podem chegar fora de ordem, e um evento atrasado pode já estar desatualizado. Antes de uma ação irreversível (liberar um produto caro, por exemplo), confirme o estado atual com [`GET /transactions/{id}`](/api-reference/transações/verificar-transação).
</Warning>

## Receptor completo

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  async function handleTransactionEvent(event) {
    const { id, event: name, data } = event;

    // deduplicação durável: insere e falha se já existir
    const firstTime = await db.webhookEvents.insertIfAbsent({ id });
    if (!firstTime) return;

    switch (name) {
      case "transaction.approved":
        if (data.type === "payment") await orders.markPaid(data.externalId);
        if (data.type === "withdrawal") await payouts.markSent(data.externalId);
        break;
      case "transaction.failed":
      case "transaction.cancelled":
        if (data.type === "payment") await orders.markClosed(data.externalId, { expired: data.expired });
        if (data.type === "withdrawal") await payouts.markFailed(data.externalId);
        break;
    }
  }
  ```

  ```python Python theme={"dark"}
  def handle_transaction_event(event: dict) -> None:
      event_id, name, data = event["id"], event["event"], event["data"]

      # deduplicação durável: insere e retorna False se já existir
      if not db.webhook_events.insert_if_absent(event_id):
          return

      if name == "transaction.approved":
          if data["type"] == "payment":
              orders.mark_paid(data["externalId"])
          elif data["type"] == "withdrawal":
              payouts.mark_sent(data["externalId"])
      elif name in ("transaction.failed", "transaction.cancelled"):
          if data["type"] == "payment":
              orders.mark_closed(data["externalId"], expired=data.get("expired", False))
          elif data["type"] == "withdrawal":
              payouts.mark_failed(data["externalId"])
  ```

  ```php PHP theme={"dark"}
  function handleTransactionEvent(array $event): void
  {
      ['id' => $id, 'event' => $name, 'data' => $data] = $event;

      // deduplicação durável: insere e retorna false se já existir
      if (!$GLOBALS['db']->webhookEvents->insertIfAbsent($id)) {
          return;
      }

      if ($name === 'transaction.approved') {
          $data['type'] === 'payment'
              ? orders_mark_paid($data['externalId'])
              : payouts_mark_sent($data['externalId']);
      } else {
          $data['type'] === 'payment'
              ? orders_mark_closed($data['externalId'], (bool) ($data['expired'] ?? false))
              : payouts_mark_failed($data['externalId']);
      }
  }
  ```
</CodeGroup>

<Note>
  As funções `db`, `orders` e `payouts` acima são do seu sistema. O ponto é o formato: dedupe primeiro, depois a regra de negócio.
</Note>

<CardGroup cols={2}>
  <Card title="Ciclo de vida" icon="workflow" href="/guides/confiabilidade/ciclo-de-vida">
    Estados, transições e o objeto transação campo a campo.
  </Card>

  <Card title="Eventos de MED" icon="scale" href="/webhooks/med">
    Quando um pagador contesta um Pix.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.