# Cadastrar Webhook

Endpoints para criar, listar e remover webhooks de notificação.

---

## Criar Webhook

```
POST /api/external/webhooks
```

### Headers

| Header | Tipo | Obrigatório | Descrição |
|--------|------|-------------|-----------|
| `Authorization` | String | Sim | `ApiKey {client_id}:{client_secret}` |
| `Content-Type` | String | Sim | `application/json` |
| `hmac` | String | Sim | Assinatura HMAC-SHA512 do body ([saiba mais](/hmac)) |

### Request Body

| Campo | Tipo | Obrigatório | Padrão | Descrição |
|-------|------|-------------|--------|-----------|
| `url` | String | Sim | -- | URL para receber notificações (HTTPS por padrão) |
| `events` | Array | **Sim** | -- | Lista de eventos para assinar. Deve ser um array **não-vazio** com pelo menos um evento válido da tabela abaixo. Omitir o campo retorna `400 {"errors": {"events": ["can't be blank"]}}`. |
| `secret` | String | Não | auto-gerado | Chave para assinatura HMAC-SHA256 das entregas. Se omitido, um valor aleatório é gerado automaticamente. |
| `description` | String | Não | `null` | Descrição livre do webhook para identificação interna |
| `allow_insecure` | Boolean | Não | `false` | Permite URLs HTTP (não-HTTPS). A segurança dos dados é responsabilidade do cliente. |

Eventos disponíveis (PIX, TEF entre contas Monetarie e teste operacional de webhook):

| Evento | Status body | Descrição | Disparo |
|--------|-------------|-----------|---------|
| `pix.charge.created` | `created` | QR code gerado ou cash-in iniciado | **Ativo** |
| `pix.charge.paid` | `paid` | PIX recebido e liquidado | **Ativo** |
| `pix.charge.expired` | `expired` | QR code expirou sem pagamento | **Ativo** |
| `pix.charge.cancelled` | `cancelled` | QR code cancelado antes de pagamento | Registrado, ainda não disparado |
| `pix.payout.queued` | `queued` | PIX enviado enfileirado por limite operacional | **Ativo** |
| `pix.payout.processing` | `processing` | PIX enviado, aguardando confirmação | **Ativo** |
| `pix.payout.confirmed` | `settled` | PIX enviado e confirmado (**terminal**) | **Ativo** |
| `pix.payout.failed` | `rejected` | PIX enviado rejeitado (**terminal**) | **Ativo** |
| `pix.payout.returned` | `returned` | PIX enviado devolvido | **Ativo** |
| `pix.refund.requested` | `requested` | Pedido de devolução recebido (infração BACEN); bloqueio cautelar criado no saldo do cliente | **Ativo** |
| `pix.refund.completed` | `settled` / `completed` | Análise da defesa finalizada e devolução executada (ou liberada) | **Ativo** |
| `pix.return.received` | `settled` | Devolução PIX recebida (crédito) | **Ativo** |
| `pix.infraction.created` | `ACKNOWLEDGED` | Infração PIX reportada pela contraparte via BACEN DICT | **Ativo** |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infração resolvida por decisão final ou cancelamento da contraparte | **Ativo** |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defesa submetida pelo merchant | **Ativo** |
| `tef.transfer.sent` | `settled` | TEF entre contas Monetarie liquidada para a conta de origem | **Ativo** |
| `tef.transfer.received` | `settled` | TEF entre contas Monetarie liquidada para a conta de destino | **Ativo** |
| `tef.transfer.failed` | `failed` | TEF entre contas Monetarie rejeitada ou não liquidada | **Ativo** |
| `webhook.test` | `test` | Teste manual. **Disponível apenas via portal Admin/Merchant - não há endpoint External API para disparar** | Disparo manual |

::: warning `pix.charge.cancelled` ainda não é disparado
O evento pode ser assinado, mas ainda não há fluxo público de cancelamento de QR code que dispare notificações desse tipo. Você pode incluí-lo no array `events` - a API aceita - porém nenhuma notificação chegará ao seu endpoint até que o fluxo seja disponibilizado.
:::

::: tip Qualquer evento fora dessa tabela é rejeitado
Ao criar um webhook, cada evento solicitado é validado contra a lista pública de eventos suportados. Eventos desconhecidos (`boleto.paid`, `account.created`, `sta.file.*`, etc.) retornam `400` com erro `events: contains invalid events: ...`.
:::

::: tip Payloads de cada evento
Exemplos de payload completos para cada evento estão em [Payloads dos Webhooks](/webhooks-payloads).
:::

### Exemplo

```bash
BODY='{"url":"https://seusite.com.br/webhook","events":["pix.charge.paid","pix.payout.confirmed"]}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')

curl -X POST https://api.monetarie.com/api/external/webhooks \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "hmac: $HMAC" \
  -d "$BODY"
```

### Resposta de Sucesso (201)

```json
{
  "worked": true,
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "url": "https://seusite.com.br/webhook",
  "events": ["pix.charge.paid", "pix.payout.confirmed"],
  "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "description": null,
  "is_active": true,
  "created_at": "2026-03-07T15:30:00Z"
}
```

::: tip Formato do `id`
O `id` do webhook é um UUID v4 canônico (36 caracteres com hífens). Use esse valor direto em `DELETE /api/external/webhooks/:id`.
:::

### Resposta de Erro (422)

```json
{
  "worked": false,
  "detail": "URL deve utilizar HTTPS"
}
```

::: warning Apenas HTTPS por Padrão
A URL do webhook deve utilizar HTTPS. URLs com HTTP serão rejeitadas, a menos que `allow_insecure: true` seja enviado no cadastro.
:::

::: danger Importante - Secret do Webhook
O campo `secret` retornado na resposta de cadastro é a chave usada para assinar as entregas do webhook (HMAC-SHA256). **Armazene esse valor com segurança** assim que receber - é ele quem valida que uma notificação veio realmente da Monetarie.

**NÃO confunda com `client_secret`:**
- `client_secret` = autenticação das suas requisições à API (header Authorization)
- `secret` do webhook = verificação da assinatura das entregas recebidas (header X-Monetarie-Signature)

Se você não enviar o campo `secret` no cadastro, um valor aleatório será gerado automaticamente e retornado na resposta.

Veja [Validação de Webhooks](/webhooks) para exemplos de como verificar a assinatura.
:::

::: tip Recuperando o secret depois
O `secret` é retornado tanto em `POST /api/external/webhooks` (criação) quanto em `GET /api/external/webhooks` e `GET /api/external/webhooks/:id` (consulta). Se você perdeu o valor, basta consultar o webhook novamente via GET.

Em uma futura versão este comportamento pode ser restringido (exibir apenas na criação); recomendamos armazenar o secret em um segredo gerenciado (vault, SSM, etc.) no momento do cadastro.
:::

### URLs HTTP

Por padrão, webhooks exigem HTTPS para garantir a segurança dos dados em trânsito. Para utilizar HTTP, envie `allow_insecure: true` no cadastro do webhook.

::: warning Atenção
URLs HTTP transmitem dados sem criptografia. A segurança e o sigilo das informações trafegadas ficam sob inteira responsabilidade do cliente. A Monetarie realiza a entrega do webhook normalmente, porém não se responsabiliza por interceptação ou vazamento de dados em conexões não criptografadas.
:::

::: warning URLs privadas são sempre bloqueadas
Mesmo com `allow_insecure: true`, URLs apontando para endereços privados/internos são rejeitadas:
- `localhost` / `127.x.x.x`
- RFC1918: `10.x.x.x`, `192.168.x.x`, `172.16-31.x.x`
- TLDs internos: `.local`, `.internal`

Webhooks devem apontar para URLs públicas acessíveis pela internet.
:::

---

## Listar Webhooks

```
GET /api/external/webhooks
```

### Headers

| Header | Tipo | Obrigatório | Descrição |
|--------|------|-------------|-----------|
| `Authorization` | String | Sim | `ApiKey {client_id}:{client_secret}` |

### Exemplo

```bash
curl -X GET https://api.monetarie.com/api/external/webhooks \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"
```

### Resposta de Sucesso (200)

Retorna um **array** de objetos (não envelopado em `{"worked": true}`). Cada item contém os mesmos campos do cadastro, incluindo `secret`.

```json
[
  {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "url": "https://seusite.com.br/webhook",
    "events": ["pix.charge.paid", "pix.payout.confirmed"],
    "description": null,
    "account_id": 10014,
    "is_active": true,
    "allow_insecure": false,
    "status": "active",
    "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "created_at": "2026-03-07T15:30:00",
    "updated_at": "2026-03-07T15:30:00"
  }
]
```

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `id` | string (UUID) | Identificador do webhook |
| `url` | string | URL de destino |
| `events` | array | Eventos assinados |
| `description` | string ou null | Descrição opcional |
| `account_id` | integer ou null | Conta associada. `null` = webhook global para a API key (se suportado) |
| `is_active` | boolean | `false` = webhook pausado, nenhuma delivery é disparada |
| `allow_insecure` | boolean | `true` = URLs HTTP aceitas |
| `status` | string | Derivado de `is_active` - `"active"` ou `"inactive"` |
| `secret` | string | **Chave HMAC-SHA256** usada para assinar entregas. Retornada no LIST para permitir recuperação caso o cliente tenha perdido o valor original |
| `created_at` / `updated_at` | string ISO 8601 | Timestamps em UTC, formato `NaiveDateTime` (sem sufixo `Z`, ex: `"2026-03-07T15:30:00"`). Diferente de outros campos de data em payloads de webhook (`paid_at`, `returned_at`, `expired_at`) que usam `DateTime` ISO 8601 com `Z` final. Sempre assuma UTC para os campos do webhook object |

---

## Remover Webhook

```
DELETE /api/external/webhooks/:id
```

### Headers

| Header | Tipo | Obrigatório | Descrição |
|--------|------|-------------|-----------|
| `Authorization` | String | Sim | `ApiKey {client_id}:{client_secret}` |

### Path Parameters

| Parâmetro | Tipo | Obrigatório | Descrição |
|-----------|------|-------------|-----------|
| `id` | String (UUID) | Sim | ID do webhook (UUID v4 retornado pelo endpoint de criação) |

### Exemplo

```bash
curl -X DELETE https://api.monetarie.com/api/external/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"
```

### Resposta de Sucesso (204)

**HTTP 204 No Content** - body vazio. O webhook foi removido com sucesso; nenhuma delivery pendente será disparada.

::: tip Primeira chamada: 204. Subsequentes: 404
A primeira chamada bem-sucedida retorna `204 No Content`. Chamadas subsequentes com o mesmo `id` retornam `404 { "errors": { "not_found": "webhook not found" } }` - o webhook já foi removido. Isso **não é idempotência estrita HTTP** (em que toda chamada retornaria 204) - é o comportamento padrão de DELETE quando o recurso deixa de existir. Escreva sua integração para aceitar tanto `204` quanto `404` como "o webhook não está mais ativo".
:::

### Resposta de Erro (400)

Formato de `id` inválido (não é UUID):

```json
{
  "errors": {
    "bad_request": "id must be a valid UUID"
  }
}
```

### Resposta de Erro (404)

Webhook não existe ou não pertence à sua conta:

```json
{
  "errors": {
    "not_found": "webhook not found"
  }
}
```
