Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,48 @@ e este projeto segue [Versionamento Semântico](https://semver.org/lang/pt-BR/sp

## [Unreleased]

## [3.1.0] — 2026-07-03

### Adicionado

- Métodos account-scoped em `WebhooksResource`, alinhados ao contrato real da
API (`/v2/webhooks`, confirmado por sondas ao vivo em 3 contas,
2026-07-02/03): `listAccountWebhooks()`, `createAccountWebhook()`,
`retrieveAccountWebhook()`, `updateAccountWebhook()`,
`deleteAccountWebhook()`, `deleteAllAccountWebhooks()` (destrutivo — remove
TODOS os webhooks da conta), `pingAccountWebhook()` e `fetchEventTypes()`
(lista viva de event types). Create/update enviam o envelope obrigatório
`{"webHook": {...}}` e as respostas envelopadas são desembrulhadas
(`fix-account-webhooks-contract`).
- Novo DTO `Nfe\Resource\Dto\Webhooks\AccountWebhook` com o shape real do
recurso: `id`, `uri`, `contentType`, `secret` (32–64 chars; ecoado no
create, omitido nas leituras), `filters`, `insecureSsl`, `headers`,
`properties`, `status`, `createdOn`, `modifiedOn`, `raw`. Nota de contrato:
o spec OpenAPI declara `contentType`/`status` como enum int, mas a API
serializa strings (`"json"`, `"Active"`) — o DTO segue o fio, e um teste de
alinhamento YAML↔DTO pina o desvio.

### Depreciado

- Os métodos company-scoped de `WebhooksResource` (`list`, `create`,
`retrieve`, `update`, `delete`, `test`): a rota
`/v1/companies/{id}/webhooks` retorna **404 incondicional** na API atual
(o contrato havia sido herdado de alucinação do SDK Node). Comportamento
inalterado; use os equivalentes `*AccountWebhook*`. Remoção na próxima
major.
- `WebhooksResource::getAvailableEvents()`: os literais `invoice.*` não
existem na API — os ids reais seguem `service_invoice.*` /
`product_invoice.*` / `consumer_invoice.*`. Use `fetchEventTypes()`.
- DTO `Nfe\Resource\Dto\Webhooks\Webhook` (`url`/`events`): shape rejeitado
pela API (`400 "The Uri field is required"`). Use `AccountWebhook`.

### Documentação

- Gotchas do contrato real documentados no phpdoc, README e
`docs/(recursos/)webhooks.md`: a NFE.io **pinga a `uri` na criação** e
exige resposta 2xx; `PUT` de update é **substituição integral** (update sem
`status` desativa o webhook — monte o corpo a partir do retrieve).

## [3.0.0] — 2026-07-01

### Removido
Expand Down
51 changes: 35 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,23 +550,41 @@ foreach ($operacoes->items as $cod) {

### Configurar um webhook

Webhooks são registrados por **conta** (`/v2/webhooks`) — todas as empresas da
conta disparam para os mesmos alvos. Na criação, a NFE.io **pinga a URL** e
exige resposta 2xx.

```php
$webhook = $nfe->webhooks->create($companyId, [
'url' => 'https://meuapp.com.br/api/webhooks/nfe',
'events' => ['invoice.issued', 'invoice.cancelled', 'invoice.error'],
'active' => true,
$webhook = $nfe->webhooks->createAccountWebhook([
'uri' => 'https://meuapp.com.br/api/webhooks/nfe',
'contentType' => 'json',
'secret' => $_ENV['NFE_WEBHOOK_SECRET'], // 32–64 caracteres
'filters' => ['service_invoice.issued_successfully'],
]);

// Listar / atualizar / remover / testar
$lista = $nfe->webhooks->list($companyId);
$nfe->webhooks->update($companyId, $webhookId, ['events' => ['invoice.issued']]);
$nfe->webhooks->delete($companyId, $webhookId);
$nfe->webhooks->test($companyId, $webhookId);
// Listar / consultar / remover / testar
$lista = $nfe->webhooks->listAccountWebhooks();
$hook = $nfe->webhooks->retrieveAccountWebhook($webhook->id);
$nfe->webhooks->pingAccountWebhook($webhook->id);
$nfe->webhooks->deleteAccountWebhook($webhook->id);

// Atualizar: PUT é substituição INTEGRAL — campos omitidos voltam ao padrão
// (um update sem `status` desativa o webhook). Parta sempre do retrieve:
$atual = $nfe->webhooks->retrieveAccountWebhook($webhookId);
$nfe->webhooks->updateAccountWebhook($webhookId, [
'uri' => 'https://meuapp.com.br/api/webhooks/nfe/v2',
'contentType' => $atual->contentType,
'status' => $atual->status,
'filters' => $atual->filters,
]);

// Listar eventos suportados pela API
$eventos = $nfe->webhooks->getAvailableEvents();
// Listar os event types suportados (lista viva da API):
$eventos = $nfe->webhooks->fetchEventTypes();
```

> Os métodos por empresa (`list($companyId)`, `create($companyId, …)`, …) estão
> **deprecated**: a rota `/v1/companies/{id}/webhooks` retorna 404 na API atual.

### Verificar assinatura no endpoint

O SDK fornece um helper estático alinhado ao esquema canônico usado pela NFE.io (HMAC-SHA1 sobre `X-Hub-Signature`):
Expand All @@ -587,12 +605,13 @@ try {
exit;
}

// Roteamento por tipo de evento
// Roteamento por tipo de evento (padrão resource.event_action —
// a lista viva vem de $nfe->webhooks->fetchEventTypes())
match ($event->type) {
'invoice.issued' => emitidaHandler($event->data),
'invoice.cancelled' => canceladaHandler($event->data),
'invoice.error' => erroHandler($event->data),
default => null,
'service_invoice.issued_successfully' => emitidaHandler($event->data),
'service_invoice.cancelled_successfully' => canceladaHandler($event->data),
'service_invoice.issued_error' => erroHandler($event->data),
default => null,
};

http_response_code(200);
Expand Down
89 changes: 64 additions & 25 deletions docs/recursos/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ title: Webhooks (CRUD) no SDK PHP da NFE.io
sidebar_label: Webhooks (CRUD)
sidebar_position: 9
slug: webhooks
description: Crie, liste, atualize, teste e exclua alvos de entrega de webhook por empresa com $nfe->webhooks — a verificação de assinatura fica na classe estática Nfe\Webhook.
description: Crie, liste, atualize, pingue e exclua alvos de entrega de webhook da conta com $nfe->webhooks — a verificação de assinatura fica na classe estática Nfe\Webhook.
---

# Webhooks (`webhooks`)

`$nfe->webhooks` gerencia os **alvos de entrega** de webhook por empresa —
URLs que a NFE.io chama quando eventos acontecem (nota emitida, cancelada,
documento de entrada recebido…). Escopo de **empresa**, família `main`
(`api.nfe.io` `/v1`), chave principal.
`$nfe->webhooks` gerencia os **alvos de entrega** de webhook — URLs que a
NFE.io chama quando eventos acontecem (nota emitida, cancelada, documento de
entrada recebido…). Webhooks são registrados por **conta** (não por empresa):
`api.nfe.io` `/v2/webhooks`, chave principal. Todas as empresas da conta
disparam para os mesmos alvos; use `filters` para escolher os eventos.

:::warning CRUD aqui, verificação lá
Este recurso é só o **CRUD** dos alvos. A verificação de assinatura das
Expand All @@ -23,50 +24,88 @@ entregas (`HMAC-SHA1`, header `X-Hub-Signature`) fica na classe **estática**

| Método | Descrição | Retorno |
|---|---|---|
| `list($companyId)` | Lista os webhooks da empresa. | `ListResponse` |
| `create($companyId, $data)` | Registra um novo alvo. | `Webhook` |
| `retrieve($companyId, $webhookId)` | Consulta por id. | `Webhook` |
| `update($companyId, $webhookId, $data)` | Atualiza URL/eventos/segredo. | `Webhook` |
| `delete($companyId, $webhookId)` | Exclui. | `void` |
| `test($companyId, $webhookId)` | Dispara uma entrega de teste. | `array` |
| `getAvailableEvents()` | Lista de eventos suportados (hard-coded, sem HTTP). | `array` |
| `listAccountWebhooks()` | Lista os webhooks da conta. | `ListResponse<AccountWebhook>` |
| `createAccountWebhook($data)` | Registra um novo alvo. | `AccountWebhook` |
| `retrieveAccountWebhook($webhookId)` | Consulta por id. | `AccountWebhook` |
| `updateAccountWebhook($webhookId, $data)` | Substitui o webhook (PUT integral — veja o aviso). | `AccountWebhook` |
| `deleteAccountWebhook($webhookId)` | Exclui um webhook. | `void` |
| `deleteAllAccountWebhooks()` | ⚠️ Exclui **TODOS** os webhooks da conta. | `void` |
| `pingAccountWebhook($webhookId)` | Dispara uma entrega de teste. | `void` |
| `fetchEventTypes()` | Lista viva de event types da API. | `list<string>` |

Os métodos por empresa (`list($companyId)`, `create($companyId, …)`, `retrieve`,
`update`, `delete`, `test`) e o `getAvailableEvents()` estão **deprecated**: a
rota `/v1/companies/{id}/webhooks` retorna 404 na API atual e os literais
`invoice.*` não existem. Migre para os equivalentes de conta acima.

## Exemplos

### Registrar um alvo de entrega

```php
$webhook = $nfe->webhooks->create('55df4dc6b6cd9007e4f13ee8', [
'url' => 'https://minha-app.example.com/webhooks/nfe',
'secret' => $_ENV['NFE_WEBHOOK_SECRET'],
$webhook = $nfe->webhooks->createAccountWebhook([
'uri' => 'https://minha-app.example.com/webhooks/nfe',
'contentType' => 'json',
'secret' => $_ENV['NFE_WEBHOOK_SECRET'], // 32–64 caracteres
'filters' => ['service_invoice.issued_successfully'],
]);
```

:::warning A URL é verificada na criação
No `create`, a NFE.io **pinga a `uri`** com um `POST` e exige resposta **2xx**.
Se o seu endpoint ainda não estiver no ar (ou exigir a assinatura para
responder 200), a criação falha.
:::

Guarde o mesmo `secret` na sua aplicação — ele é o HMAC usado para assinar cada
entrega, e você o passa a `Nfe\Webhook::verifySignature()` no seu endpoint.
entrega, e você o passa a `Nfe\Webhook::verifySignature()` no seu endpoint. O
`secret` é **ecoado apenas no create**; `retrieve`/`list` o omitem.

### Testar, atualizar e excluir
### Atualizar — PUT é substituição integral

:::warning Sempre parta do estado atual
`updateAccountWebhook()` faz um `PUT` que **substitui o webhook inteiro**:
campos omitidos voltam ao padrão. Um update sem `status` **desativa o
webhook**. Monte o corpo a partir do `retrieve`:
:::

```php
// Entrega de teste para validar o endpoint de ponta a ponta:
$nfe->webhooks->test($companyId, $webhook->id);
$atual = $nfe->webhooks->retrieveAccountWebhook($webhook->id);

$nfe->webhooks->update($companyId, $webhook->id, [
'url' => 'https://minha-app.example.com/webhooks/nfe/v2',
$nfe->webhooks->updateAccountWebhook($webhook->id, [
'uri' => 'https://minha-app.example.com/webhooks/nfe/v2', // a mudança
'contentType' => $atual->contentType,
'status' => $atual->status,
'filters' => $atual->filters,
]);
```

$nfe->webhooks->delete($companyId, $webhook->id); // void
### Testar e excluir

```php
// Entrega de teste (PUT /v2/webhooks/{id}/pings, responde 204):
$nfe->webhooks->pingAccountWebhook($webhook->id);

$nfe->webhooks->deleteAccountWebhook($webhook->id); // void

// ⚠️ Destrutivo e sem undo — remove TODOS os alvos da conta:
$nfe->webhooks->deleteAllAccountWebhooks();
```

### Eventos disponíveis

```php
// Lista embutida no SDK — não faz chamada de rede:
$eventos = $nfe->webhooks->getAvailableEvents();
// Lista viva da API (GET /v2/webhooks/eventTypes) — 46 ids no padrão
// resource.event_action, ex.: service_invoice.issued_successfully,
// product_invoice.issued_error, consumer_invoice.cancelled_successfully…
$eventos = $nfe->webhooks->fetchEventTypes();
```

Use esses ids em `filters` ao criar/atualizar um webhook. Sem `filters`, o
alvo recebe todos os eventos.

## Veja também

- [Guia de webhooks](../webhooks.md) — verificação de assinatura e idempotência.
- [Emissão assíncrona e polling](../async-and-polling.md) — push vs. polling.
- [Empresas](./companies.md) — o escopo dos alvos.
- [Empresas](./companies.md) — as empresas da conta que geram os eventos.
2 changes: 1 addition & 1 deletion docs/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ $event = Webhook::constructEvent(
secret: $_ENV['NFE_WEBHOOK_SECRET'],
);

$event->type; // ex.: "invoice.issued" (a chave `action` do envelope v2)
$event->type; // ex.: "service_invoice.issued_successfully" (a chave `action` do envelope v2)
$event->data; // array com o payload (a chave `payload` do envelope)
$event->id; // id estável para deduplicação, ou null
$event->createdAt; // timestamp ISO-8601 da entrega, ou null
Expand Down
11 changes: 6 additions & 5 deletions samples/webhook-verify.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@
exit;
}

// Eventos típicos: invoice.issued, invoice.cancelled, invoice.error
// Eventos no padrão resource.event_action — a lista viva vem de
// $nfe->webhooks->fetchEventTypes() (ex.: service_invoice.*, product_invoice.*).
error_log("Webhook ok: type={$event->type}, id=" . ($event->id ?? '-'));

match ($event->type) {
'invoice.issued' => error_log(' -> nota emitida'),
'invoice.cancelled' => error_log(' -> nota cancelada'),
'invoice.error' => error_log(' -> nota com erro'),
default => error_log(" -> evento não tratado: {$event->type}"),
'service_invoice.issued_successfully' => error_log(' -> nota emitida'),
'service_invoice.cancelled_successfully' => error_log(' -> nota cancelada'),
'service_invoice.issued_error' => error_log(' -> nota com erro'),
default => error_log(" -> evento não tratado: {$event->type}"),
};

http_response_code(200);
Expand Down
12 changes: 7 additions & 5 deletions skills/nfeio-php-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ All 17 resources are bound eagerly as `public readonly` properties on `Nfe\Clien
| `$nfe->companies` | Companies | api.nfe.io /v1 | Account |
| `$nfe->legalPeople` | Legal people (PJ) | api.nfe.io /v1 | Company |
| `$nfe->naturalPeople` | Natural people (PF) | api.nfe.io /v1 | Company |
| `$nfe->webhooks` | Webhooks CRUD | api.nfe.io /v1 | Company |
| `$nfe->webhooks` | Webhooks CRUD | api.nfe.io /v2 | Account |
| `$nfe->addresses` | Address lookup (CEP only) | address.api.nfe.io/v2 | Global |
| `$nfe->legalEntityLookup` | CNPJ lookup | legalentity.api.nfe.io | Global |
| `$nfe->naturalPersonLookup` | CPF lookup | naturalperson.api.nfe.io | Global |
Expand Down Expand Up @@ -218,7 +218,9 @@ Internally, downloads transparently follow a 302/303/307 redirect to the CDN in

## Core Pattern: Webhooks

CRUD lives on `$nfe->webhooks`; **signature validation lives on the static `Nfe\Webhook` class** (do not look for `$nfe->webhooks->verifySignature()` — it does not exist):
CRUD lives on `$nfe->webhooks` and is **account-scoped** (`/v2/webhooks` — all companies of the account fire to the same targets): `listAccountWebhooks()`, `createAccountWebhook($data)`, `retrieveAccountWebhook($id)`, `updateAccountWebhook($id, $data)`, `deleteAccountWebhook($id)`, `deleteAllAccountWebhooks()` (destructive — removes ALL), `pingAccountWebhook($id)`, `fetchEventTypes()`. Gotchas: NFE.io **pings the `uri` on create and requires a 2xx**; `secret` is 32–64 chars, echoed only on create; **update is a full PUT replacement** — omitting `status` deactivates the hook, so build the body from a fresh retrieve. The company-scoped `list`/`create`/`retrieve`/`update`/`delete`/`test($companyId, …)` methods are `@deprecated` (their `/v1/companies/{id}/webhooks` route 404s on the live API).

**Signature validation lives on the static `Nfe\Webhook` class** (do not look for `$nfe->webhooks->verifySignature()` — it does not exist):

```php
use Nfe\Webhook;
Expand All @@ -232,7 +234,7 @@ $ok = Nfe\Webhook::verifySignature($payload, $signature, $secret); // bool; HMAC
$event = Nfe\Webhook::constructEvent($payload, $signature, $secret); // Nfe\WebhookEvent
```

`verifySignature()` accepts both prefixed (`sha1=…`) and bare hex, and takes an optional `$algo` (default `'sha1'`). `$nfe->webhooks->getAvailableEvents()` returns the hard-coded event list.
`verifySignature()` accepts both prefixed (`sha1=…`) and bare hex, and takes an optional `$algo` (default `'sha1'`). Event-type ids follow `resource.event_action` (e.g. `service_invoice.issued_successfully`) — fetch the live list with `$nfe->webhooks->fetchEventTypes()`; the hard-coded `getAvailableEvents()` is `@deprecated` (its `invoice.*` literals do not exist on the live API).

## Critical Pitfalls

Expand All @@ -247,7 +249,7 @@ Verified against the SDK source. These are exactly where a Node-to-PHP port goes
7. **`productInvoices->list()` requires `environment`** and its `$options` arg is mandatory (cursor pagination). `serviceInvoices`/`consumerInvoices` use page-style with optional `$options`.
8. **`getStatus()` exists only on `serviceInvoices`** — not on product/consumer.
9. **Downloads return `string` bytes**, not a Buffer/stream.
10. **Deletion method names differ**: `companies->remove()` returns `array{deleted, id}`; `legalPeople`/`naturalPeople`/`stateTaxes`/`webhooks` use `delete(): void`.
10. **Deletion method names differ**: `companies->remove()` returns `array{deleted, id}`; `legalPeople`/`naturalPeople`/`stateTaxes` use `delete(): void`; webhooks use `deleteAccountWebhook($id): void` — and `deleteAllAccountWebhooks()` wipes EVERY webhook on the account, so never confuse the two.
11. **`federalTaxNumber` DTO type varies**: `Company` and `LegalPerson` = `?int`; `NaturalPerson` = `?string`. The SDK does no create-side validation/coercion — send the type the API expects (int for company/legal person).
12. **`dataApiKey` only applies to `addresses`, `legalEntityLookup`, `naturalPersonLookup`, and NF-e/NFC-e query.** `taxCalculation`, `transportationInvoices`, `inboundProductInvoices` always use the main `apiKey` — porting Node code that relied on `dataApiKey` for `api.nfse.io` resources will silently send the main key.
13. **Certificate upload is not implemented** — `companies` has only read-side cert helpers (`getCertificateStatus`, `checkCertificateExpiration`, `getCompaniesWith[Expiring]Certificates`). No `uploadCertificate`/`validateCertificate` (transport is JSON-only, no multipart).
Expand All @@ -273,7 +275,7 @@ Verified against the SDK source. These are exactly where a Node-to-PHP port goes
| Calculate Brazilian taxes | `$nfe->taxCalculation->calculate($tenantId, $request)` |
| Manage companies & read cert status | `$nfe->companies->*` |
| Manage people (PJ/PF) | `$nfe->legalPeople->*` / `$nfe->naturalPeople->*` |
| Set up webhook notifications | `$nfe->webhooks->create($companyId, $data)` |
| Set up webhook notifications | `$nfe->webhooks->createAccountWebhook($data)` (account-scoped; API pings the `uri`, expects 2xx) |
| Validate an incoming webhook | `Nfe\Webhook::verifySignature($rawBody, $sig, $secret)` |
| Manage state tax registrations (IE) | `$nfe->stateTaxes->*` |
| Cancel a service invoice | `$nfe->serviceInvoices->cancel($companyId, $invoiceId)` (synchronous; returns the DTO) |
Expand Down
Loading