Pular para o conteúdo
getnextpdf.com

Enterprise edição

Webhook — Referência Profunda

O namespace NextPDF\Enterprise\Webhook fornece entrega de webhook com escopo por locatário para eventos de job. A superfície pública são seis símbolos: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy e DeadLetterEntry. O gerenciador registra endpoints por locatário e despacha eventos de job para os registros assinantes. O engine de entrega faz POST de um payload JSON assinado com HMAC-SHA256, valida cada destino contra o gate de egress SSRF do Core, tenta novamente com backoff exponencial e registra falhas permanentes em uma fila de dead-letter em memória. A partir da 3.1.0, a assinatura vincula o cabeçalho X-NextPDF-Timestamp na base string do MAC, de modo que os receptores verificam frescor e integridade juntos. Para o guia em nível de workflow, consulte Webhook.

Esta capacidade é fornecida no NextPDF Enterprise (nextpdf/enterprise) e é ativada com um envelope de licença de nível Enterprise. Uma implantação sem essa titularidade não carrega as classes da capacidade. Compare edições e obtenha uma licença.

A superfície de webhook é uma capacidade base do Enterprise, disponível assim que o pacote Enterprise é instalado; não há nenhum sinalizador por recurso separado. O NextPDF Core (Apache-2.0) e o NextPDF Pro não têm nenhuma superfície de registro ou entrega de webhook; o gerenciador, o registro, o payload, o engine de entrega, a política de retry e a entrada de dead-letter são fornecidos apenas em nextpdf/enterprise.

SímboloParâmetrosComportamento padrãoRetornaLança ou falha comNotas
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullCria um gerenciador com um índice de registros vazio em memóriaNovo WebhookManagerNão lançaOs registros são indexados por locatário
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationAnexa o registro ao índice do locatário chamadorvoidInvalidArgumentException quando o locatário do registro não corresponde ao locatário do contextoO registro entre locatários é rejeitado antes do armazenamento
WebhookManager::unregisterTenantContext $tenant, string $registrationIdSubstitui o registro correspondente por uma cópia desativadaboolNão lança; retorna false quando o id não é encontradoDesativação suave; o histórico é preservado
WebhookManager::activeRegistrationsTenantContext $tenantFiltra os registros do locatário para os ativoslist<WebhookRegistration>Não lançaApenas os registros do locatário chamador são visíveis
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventEntrega o evento a cada registro ativo que assina o tipo de eventoint (entregas bem-sucedidas)Propaga JsonException quando os dados do evento não são codificáveis em JSON; falhas de entrega não lançamUm novo id de entrega de 32 hex é gerado por entrega de registro
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullArmazena os valores fornecidos literalmenteNovo WebhookRegistrationNenhum @throws declarado; o PHP lança TypeError em tipos de argumento incompatíveis sob strict_typesfinal readonly; $events vazio significa assinar-todos
WebhookRegistration::subscribesToJobEventType $eventTypetrue quando $events está vazio ou contém o tipoboolNão lançaComparação de identidade estrita
WebhookRegistration::deactivateRetorna uma cópia inativaselfNão lançaA instância original permanece inalterada
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdCopia id do job, tipo de evento, dados e timestamp do eventoselfNão lançaFactory estático usado por dispatch
WebhookPayload::toJsonSerializa o corpo de seis campos com barras não escapadasnon-empty-stringJsonException quando os dados do evento não são codificáveis em JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayRetorna o corpo como um array associativoarray<string, mixed>Não lançaTimestamp formatado como RFC 3339 estendido
WebhookPayload::signedTimestampTempo do evento em segundos Unix, fixado em zero ou maiorint<0, max>Não lançaEmitido como X-NextPDF-Timestamp e vinculado ao MAC
WebhookPayload::signstring $secretHMAC-SHA256 sobre a base string {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException via toJson() quando o corpo não é codificávelVincula o cabeçalho de timestamp ao corpo criptograficamente
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullEngine de entrega PSR-18/PSR-17 com uma fila de dead-letter vaziaNovo WebhookDeliveryNão lançaPolítica padrão: 5 tentativas, 1 s base, 300 s limite
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadFaz POST do payload assinado com validação de egress SSRF por tentativa e backoff exponencialboolJsonException antes da primeira tentativa quando o corpo não é codificável; caso contrário não lança — false significa que o payload foi roteado para a fila de dead-lettertrue apenas em uma resposta 2xx
WebhookDelivery::deadLettersRetorna todas as entradas registradaslist<DeadLetterEntry>Não lançaEm memória, com escopo de processo
WebhookDelivery::clearDeadLettersEsvazia a fila de dead-lettervoidNão lançaIrreversível; exporte as entradas primeiro se o replay for necessário
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Armazena os valores da políticaNovo WebhookRetryPolicyNenhum @throws declarado; os parâmetros são documentados como positive-int$maxRetries conta o total de tentativas
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), limitado a maxDelaySecondspositive-intNão lançaOs números de tentativa começam em 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue enquanto a tentativa atual está abaixo do máximoboolNão lançaA espera é pulada após a tentativa final
WebhookRetryPolicy::default5 tentativas, 1 s base, 300 s limiteselfNão lançaFactory estático; padrão de produção
WebhookRetryPolicy::aggressive10 tentativas, 2 s base, 600 s limiteselfNão lançaFactory estático para endpoints críticos
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseArmazena o registro de falha literalmenteNovo DeadLetterEntryNenhum @throws declarado; TypeError sob strict_typesfinal readonly; $lastHttpStatus nulo significa falha de transporte
DeadLetterEntry::markReplayedRetorna uma cópia com replayed = trueselfNão lançaMesmo id; a entrada original permanece inalterada
public function __construct(
private readonly WebhookDelivery $delivery,
private readonly ?LoggerInterface $logger = null,
) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): int
public function __construct(
public string $id,
public string $tenantId,
public string $url,
public array $events,
public string $secret,
public bool $active = true,
public ?string $description = null,
) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): self
public static function fromJobEvent(
JobEvent $event,
string $tenantId,
string $deliveryId,
): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): string
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(),
private readonly ?LoggerInterface $logger = null,
) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): void
public function __construct(
public int $maxRetries = 5,
public int $baseDelaySeconds = 1,
public int $maxDelaySeconds = 300,
) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): self
public function __construct(
public string $id,
public string $registrationId,
public WebhookPayload $payload,
public int $attempts,
public string $lastError,
public ?int $lastHttpStatus,
public DateTimeImmutable $failedAt,
public bool $replayed = false,
) {}
public function markReplayed(): self
  • Os registros são indexados por locatário. register() rejeita um registro cujo identificador de locatário não corresponde ao contexto chamador. unregister() é uma desativação suave: o registro é substituído por uma cópia inativa, preservando o histórico enquanto o exclui de dispatches futuros.
  • dispatch() itera apenas os registros ativos do locatário chamador que assinam o tipo de evento despachado. Uma lista de eventos assinados vazia significa assinar-todos. O valor de retorno conta as entregas bem-sucedidas.
  • Cada entrega é um HTTP POST com um corpo JSON e cinco cabeçalhos: Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (segundos unix), X-NextPDF-Delivery-Id e X-NextPDF-Event.
  • Os campos do corpo JSON são delivery_id, job_id, event_type, data, timestamp (RFC 3339 estendido) e tenant_id, serializados com barras não escapadas. Os valores de tipo de evento vêm de JobEventType em nextpdf/core: progress, completed, failed, cancelled.
  • Esquema de assinatura (alterado na 3.1.0, breaking). A base string do HMAC-SHA256 é {signedTimestamp}.{jsonBody}, com chave igual ao segredo do registro — não apenas o corpo. O valor de X-NextPDF-Timestamp é o componente de timestamp do MAC, de modo que um cabeçalho de timestamp adulterado ou reproduzido invalida a assinatura.
  • Verificação do receptor: leia o cabeçalho X-NextPDF-Timestamp T; rejeite quando T estiver fora de uma janela de frescor aceitável (por exemplo, 300 s); recalcule hash_hmac('sha256', T . '.' . rawBody, secret) sobre os bytes brutos recebidos; compare em tempo constante com o valor do cabeçalho após remover o prefixo sha256=.
  • O corpo, a assinatura e o id de entrega são calculados uma vez por entrega e permanecem constantes entre as tentativas de retry.
  • Gate de egress SSRF. Antes de cada tentativa, a URL de destino passa pelo gate UrlValidator::validateExternalUrl() do Core: apenas esquema HTTPS; faixas de loopback, privadas, reservadas, carrier-grade-NAT, de metadados de nuvem e de transição IPv6 com IPv4 embutido são bloqueadas; os hostnames são resolvidos por DNS (A e AAAA) e hosts não resolvíveis são rejeitados com fail-closed. Uma URL bloqueada nunca é enviada: o loop de tentativas aborta e o payload é roteado diretamente para a fila de dead-letter com um último erro Blocked SSRF destination: e um status HTTP nulo.
  • Classificação de resultado por tentativa: 2xx é sucesso e retorna imediatamente; um 4xx diferente de 429 é terminal e vai direto para dead-letter; todo outro resultado — 3xx, 429, 5xx ou uma exceção de transporte — é passível de retry até a contagem total de tentativas da política.
  • O backoff é exponencial: a espera antes da próxima tentativa é baseDelaySeconds × 2^(attempt − 1), limitada a maxDelaySeconds. A espera é pulada após a tentativa final.
  • Quando nenhuma tentativa é bem-sucedida, um DeadLetterEntry registra um id único, o id do registro, o payload original, a contagem de tentativas (fixada no máximo da política), a última mensagem de erro, o último status HTTP (nulo em falha de transporte ou bloqueio SSRF) e o timestamp da falha.
  • A fila de dead-letter é em memória e tem escopo da duração do processo. markReplayed() produz uma cópia sinalizada; ela não reenvia, e a fila mantém a entrada original.
  • Lista de eventos vazia. O registro recebe todo tipo de evento. Defina o escopo da lista explicitamente quando o receptor não deve ver todos os eventos.
  • 4xx terminal versus falha de transporte. Uma rejeição 4xx registra um lastHttpStatus preenchido; uma falha de conexão registra nulo. Use o nulo para distinguir a rejeição do receptor da falha de transporte.
  • Destino bloqueado por SSRF. Um registro apontando para um endereço HTTP, privado, loopback ou de metadados vai para dead-letter na primeira tentativa com um erro Blocked SSRF destination: e status nulo. Nenhuma requisição de saída é feita. Corrija a URL e registre novamente.
  • Receptores legados após a atualização. Um receptor que ainda verifica o HMAC apenas-do-corpo anterior à 3.1.0 falha com fail-closed contra as entregas 3.1.0. Migre o receptor para a base string {timestamp}.{body} e consuma X-NextPDF-Timestamp.
  • Dados de evento não codificáveis. toJson() e sign() lançam JsonException, que se propaga para fora de deliver() e dispatch() antes que qualquer tentativa seja feita.
  • Bloqueio síncrono. deliver() dorme inline entre as tentativas. O backoff cumulativo chega a 15 s sob a política default e cerca de 17 minutos sob a política aggressive. Despache a partir de um queue worker quando a latência do receptor não é confiável.
  • Fixação da contagem de tentativas. A contagem de tentativas registrada nunca excede o máximo da política, mesmo que o contador interno do loop avance além dele no esgotamento.
  • Crescimento e durabilidade da fila. A fila de dead-letter cresce sem limites dentro do processo e desaparece na reinicialização. Exporte as entradas via deadLetters() e persista-as externamente antes de chamar clearDeadLetters() quando o replay durável for necessário.
  • O replay é conduzido pelo operador. A reentrega significa chamar deliver() novamente com o payload da entrada; markReplayed() apenas registra o fato em uma cópia.
  • Resíduo de DNS-rebinding. A URL é revalidada em cada tentativa, o que estreita mas não fecha a janela de rebinding: a abstração PSR-18 não pode fixar a conexão ao IP validado. Adicione controles de egress em nível de rede onde esse resíduo importa.
  • Tratamento do segredo. O segredo do registro é uma credencial. O HMAC autentica apenas integridade e origem — ele não é confidencialidade. Não coloque no payload do evento dados que o receptor não deve ver.

A assinatura do payload é HMAC-SHA256 através de hash_hmac() do PHP, portanto ela depende do provedor de criptografia do host. Em um build com restrição FIPS, uma primitiva não aprovada falha no limite criptográfico em vez de fazer downgrade. A camada de webhook não acrescenta nenhuma política criptográfica própria.

  • A autenticação de payload implementa HMAC, o código de autenticação de mensagem com hash e chave do FIPS PUB 198-1 §1, instanciado com SHA-256.
  • A proteção contra replay segue a orientação de segurança de webhook da OWASP Cheat Sheet Series: o timestamp do evento viaja em um cabeçalho dedicado e é semeado no cálculo da assinatura, de modo que um timestamp adulterado falha na verificação.
  • Os timestamps do corpo usam o formato de data-hora estendido RFC 3339. Declarado em código: RFC 3339 não foi recuperado do corpus RAG para esta página.
  • Estas são declarações de capacidade fundamentadas no código-fonte do produto e nas cláusulas citadas. A NextPDF não faz nenhuma alegação de conformidade ou certificação para esta superfície.
  • Todas as classes declaram strict_types=1 e são final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy e DeadLetterEntry são final readonly com propriedades públicas promovidas.
  • O módulo carrega uma anotação @since de 2.2.0; o esquema de assinatura vinculado a timestamp é uma mudança breaking documentada na 3.1.0.
  • O engine de entrega recebe abstrações PSR-18/PSR-17, então um mock de HTTP client exercita todo o caminho de envio, retry e dead-letter offline. O logger tem padrão null; injete um logger PSR-3 em produção ou as falhas aparecem apenas através dos valores de retorno.
  • As implementações do receptor devem usar hash_equals() para a comparação da assinatura e impor uma janela de frescor em X-NextPDF-Timestamp.
  • Testes de limite recomendados: registro com locatário incompatível, fan-out de lista de eventos vazia, 4xx terminal, esgotamento de retry, URL bloqueada por SSRF, rejeição de assinatura com timestamp adulterado contra um vetor fixo e fixação da contagem de tentativas de dead-letter.

Esta página documenta apenas o comportamento observável externamente e a superfície da API pública suportada. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismos, nomes de arquivo de runbook e prefixos de tíquete estão fora do escopo.