Ir al contenido
getnextpdf.com

Enterprise edición

Webhook — Referencia detallada

El espacio de nombres NextPDF\Enterprise\Webhook ofrece entrega de webhooks con ámbito de tenant para eventos de trabajos. La superficie pública consta de seis símbolos: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy y DeadLetterEntry. El gestor registra endpoints por tenant y despacha los eventos de trabajos a los registros suscritos. El motor de entrega envía por POST una carga JSON firmada con HMAC-SHA256, valida cada destino contra la puerta de egreso SSRF de Core, reintenta con retroceso exponencial y registra los fallos permanentes en una cola de mensajes fallidos en memoria. A partir de 3.1.0, la firma vincula la cabecera X-NextPDF-Timestamp a la cadena base del MAC, de modo que los receptores verifican frescura e integridad de forma conjunta. Para la guía a nivel de flujo de trabajo, véase Webhook.

Esta capacidad se incluye en NextPDF Enterprise (nextpdf/enterprise) y se activa con un sobre de licencia de nivel Enterprise. Un despliegue sin esa habilitación no carga las clases de la capacidad. Comparar ediciones y obtener una licencia.

La superficie de webhooks es una capacidad base de Enterprise, disponible una vez instalado el paquete Enterprise; no hay un indicador por función independiente. NextPDF Core (Apache-2.0) y NextPDF Pro no tienen superficie de registro ni de entrega de webhooks; el gestor, el registro, la carga, el motor de entrega, la política de reintentos y la entrada de mensajes fallidos se incluyen únicamente en nextpdf/enterprise.

SímboloParámetrosComportamiento predeterminadoDevuelveLanza o falla conNotas
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullCrea un gestor con un índice de registros en memoria vacíoNuevo WebhookManagerNo lanzaLos registros se indexan por tenant
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationAñade el registro al índice del tenant que realiza la llamadavoidInvalidArgumentException cuando el tenant del registro no coincide con el tenant del contextoEl registro entre tenants se rechaza antes del almacenamiento
WebhookManager::unregisterTenantContext $tenant, string $registrationIdReemplaza el registro coincidente por una copia desactivadaboolNo lanza; devuelve false cuando no se encuentra el idDesactivación lógica; se conserva el historial
WebhookManager::activeRegistrationsTenantContext $tenantFiltra los registros del tenant para quedarse con los activoslist<WebhookRegistration>No lanzaSolo son visibles los registros del tenant que realiza la llamada
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventEntrega el evento a cada registro activo que esté suscrito al tipo de eventoint (entregas correctas)Propaga JsonException cuando los datos del evento no son codificables en JSON; los fallos de entrega no lanzanSe genera un nuevo id de entrega de 32 hex por cada entrega de registro
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullAlmacena los valores suministrados tal cualNuevo WebhookRegistrationSin @throws declarado; PHP lanza TypeError ante tipos de argumento no coincidentes bajo strict_typesfinal readonly; $events vacío significa suscribirse a todos
WebhookRegistration::subscribesToJobEventType $eventTypetrue cuando $events está vacío o contiene el tipoboolNo lanzaComparación de identidad estricta
WebhookRegistration::deactivateDevuelve una copia inactivaselfNo lanzaLa instancia original no se modifica
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdCopia el id del trabajo, el tipo de evento, los datos y la marca de tiempo del eventoselfNo lanzaFábrica estática usada por dispatch
WebhookPayload::toJsonSerializa el cuerpo de seis campos con las barras sin escaparnon-empty-stringJsonException cuando los datos del evento no son codificables en JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayDevuelve el cuerpo como un array asociativoarray<string, mixed>No lanzaMarca de tiempo formateada como RFC 3339 extendido
WebhookPayload::signedTimestampTiempo del evento en segundos Unix, acotado a cero o superiorint<0, max>No lanzaSe emite como X-NextPDF-Timestamp y se vincula al MAC
WebhookPayload::signstring $secretHMAC-SHA256 sobre la cadena base {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException mediante toJson() cuando el cuerpo no es codificableVincula criptográficamente la cabecera de marca de tiempo al cuerpo
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullMotor de entrega PSR-18/PSR-17 con una cola de mensajes fallidos vacíaNuevo WebhookDeliveryNo lanzaPolítica predeterminada: 5 intentos, 1 s base, 300 s de tope
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadEnvía por POST la carga firmada con validación de egreso SSRF por intento y retroceso exponencialboolJsonException antes del primer intento cuando el cuerpo no es codificable; en caso contrario no lanza — false significa que la carga se enrutó a la cola de mensajes fallidostrue solo ante una respuesta 2xx
WebhookDelivery::deadLettersDevuelve todas las entradas registradaslist<DeadLetterEntry>No lanzaEn memoria, con ámbito de proceso
WebhookDelivery::clearDeadLettersVacía la cola de mensajes fallidosvoidNo lanzaIrreversible; exportar las entradas primero si se requiere reproducción
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Almacena los valores de la políticaNuevo WebhookRetryPolicySin @throws declarado; los parámetros están documentados como positive-int$maxRetries cuenta el total de intentos
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), con tope en maxDelaySecondspositive-intNo lanzaLos números de intento empiezan en 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue mientras el intento actual esté por debajo del máximoboolNo lanzaLa espera se omite tras el intento final
WebhookRetryPolicy::default5 intentos, 1 s base, 300 s de topeselfNo lanzaFábrica estática; predeterminado de producción
WebhookRetryPolicy::aggressive10 intentos, 2 s base, 600 s de topeselfNo lanzaFábrica estática para endpoints críticos
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseAlmacena el registro de fallo tal cualNuevo DeadLetterEntrySin @throws declarado; TypeError bajo strict_typesfinal readonly; $lastHttpStatus nulo significa fallo de transporte
DeadLetterEntry::markReplayedDevuelve una copia con replayed = trueselfNo lanzaMismo id; la entrada original no se modifica
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
  • Los registros se indexan por tenant. register() rechaza un registro cuyo identificador de tenant no coincide con el contexto que realiza la llamada. unregister() es una desactivación lógica: el registro se reemplaza por una copia inactiva, conservando el historial y excluyéndolo del despacho futuro.
  • dispatch() itera únicamente los registros activos del tenant que realiza la llamada que están suscritos al tipo de evento despachado. Una lista de eventos suscritos vacía significa suscribirse a todos. El valor de retorno cuenta las entregas correctas.
  • Cada entrega es un POST HTTP con un cuerpo JSON y cinco cabeceras: Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (segundos Unix), X-NextPDF-Delivery-Id y X-NextPDF-Event.
  • Los campos del cuerpo JSON son delivery_id, job_id, event_type, data, timestamp (RFC 3339 extendido) y tenant_id, serializados con las barras sin escapar. Los valores del tipo de evento provienen de JobEventType en nextpdf/core: progress, completed, failed, cancelled.
  • Esquema de firma (modificado en 3.1.0, con ruptura). La cadena base de HMAC-SHA256 es {signedTimestamp}.{jsonBody}, con la clave del secreto del registro, no el cuerpo por sí solo. El valor de X-NextPDF-Timestamp es el componente de marca de tiempo del MAC, de modo que una cabecera de marca de tiempo manipulada o reproducida invalida la firma.
  • Verificación en el receptor: leer la cabecera X-NextPDF-Timestamp T; rechazar cuando T esté fuera de una ventana de frescura aceptable (por ejemplo 300 s); recalcular hash_hmac('sha256', T . '.' . rawBody, secret) sobre los bytes recibidos en bruto; comparar en tiempo constante con el valor de la cabecera tras eliminar el prefijo sha256=.
  • El cuerpo, la firma y el id de entrega se calculan una vez por entrega y permanecen constantes a lo largo de los intentos de reintento.
  • Puerta de egreso SSRF. Antes de cada intento, la URL de destino pasa por la puerta UrlValidator::validateExternalUrl() de Core: únicamente esquema HTTPS; se bloquean los rangos de loopback, privados, reservados, carrier-grade-NAT, de metadatos de nube y de transición IPv6 con IPv4 embebida; los nombres de host se resuelven por DNS (A y AAAA) y los hosts no resolubles se rechazan a prueba de fallos. Una URL bloqueada nunca se envía: el bucle de intentos se aborta y la carga se enruta directamente a la cola de mensajes fallidos con un último error Blocked SSRF destination: y un estado HTTP nulo.
  • Clasificación del resultado por intento: 2xx es éxito y devuelve de inmediato; un 4xx distinto de 429 es terminal y va directamente a mensajes fallidos; cualquier otro resultado —3xx, 429, 5xx o una excepción de transporte— es reintentable hasta el total de intentos de la política.
  • El retroceso es exponencial: la espera antes del siguiente intento es baseDelaySeconds × 2^(attempt − 1), con tope en maxDelaySeconds. La espera se omite tras el intento final.
  • Cuando ningún intento tiene éxito, una DeadLetterEntry registra un id único, el id del registro, la carga original, el recuento de intentos (acotado al máximo de la política), el último mensaje de error, el último estado HTTP (nulo ante un fallo de transporte o un bloqueo SSRF) y la marca de tiempo del fallo.
  • La cola de mensajes fallidos está en memoria y su ámbito es la vida del proceso. markReplayed() produce una copia marcada; no reenvía, y la cola conserva la entrada original.
  • Lista de eventos vacía. El registro recibe todos los tipos de evento. Acotar la lista de forma explícita cuando el receptor no deba ver todos los eventos.
  • 4xx terminal frente a fallo de transporte. Un rechazo 4xx registra un lastHttpStatus con valor; un fallo de conexión registra nulo. Usar el valor nulo para distinguir el rechazo del receptor del fallo de transporte.
  • Destino bloqueado por SSRF. Un registro que apunta a una dirección HTTP, privada, loopback o de metadatos pasa a mensajes fallidos en el primer intento con un error Blocked SSRF destination: y estado nulo. No se realiza ninguna solicitud saliente. Corregir la URL y volver a registrar.
  • Receptores heredados tras la actualización. Un receptor que aún verifica el HMAC solo sobre el cuerpo previo a 3.1.0 falla a prueba de fallos frente a las entregas de 3.1.0. Migrar el receptor a la cadena base {timestamp}.{body} y consumir X-NextPDF-Timestamp.
  • Datos de evento no codificables. toJson() y sign() lanzan JsonException, que se propaga fuera de deliver() y dispatch() antes de realizar cualquier intento.
  • Bloqueo síncrono. deliver() espera de forma incorporada entre intentos. El retroceso acumulado alcanza 15 s con la política predeterminada y alrededor de 17 minutos con la política agresiva. Despachar desde un worker de cola cuando la latencia del receptor no sea de confianza.
  • Acotación del recuento de intentos. El recuento de intentos registrado nunca supera el máximo de la política, aunque el contador interno del bucle lo sobrepase al agotarse.
  • Crecimiento y durabilidad de la cola. La cola de mensajes fallidos crece sin límite dentro del proceso y desaparece al reiniciar. Exportar las entradas mediante deadLetters() y persistirlas externamente antes de llamar a clearDeadLetters() cuando se requiera una reproducción duradera.
  • La reproducción la dirige el operador. Reentregar significa volver a llamar a deliver() con la carga de la entrada; markReplayed() solo registra el hecho en una copia.
  • Riesgo residual de DNS rebinding. La URL se revalida en cada intento, lo que reduce pero no cierra la ventana de rebinding: la abstracción PSR-18 no puede fijar la conexión a la IP validada. Añadir controles de egreso a nivel de red allí donde este riesgo residual importe.
  • Manejo del secreto. El secreto del registro es una credencial. El HMAC autentica únicamente integridad y origen; no aporta confidencialidad. No colocar en la carga del evento datos que el receptor no deba ver.

La firma de la carga es HMAC-SHA256 mediante hash_hmac() de PHP, por lo que depende del proveedor criptográfico del host. En una compilación restringida a FIPS, una primitiva no aprobada falla en la frontera criptográfica en lugar de degradarse. La capa de webhooks no añade ninguna política criptográfica propia.

  • La autenticación de la carga implementa HMAC, el código de autenticación de mensajes basado en hash con clave de FIPS PUB 198-1 §1, instanciado con SHA-256.
  • La protección contra reproducción sigue la guía de seguridad de webhooks de OWASP Cheat Sheet Series: la marca de tiempo del evento viaja en una cabecera dedicada y se incorpora al cálculo de la firma, de modo que una marca de tiempo manipulada falla la verificación.
  • Las marcas de tiempo del cuerpo usan el formato de fecha y hora extendido RFC 3339. Declarado en código: RFC 3339 no se recuperó del corpus RAG para esta página.
  • Estas son afirmaciones de capacidad fundamentadas en el código fuente del producto y en las cláusulas citadas. NextPDF no realiza ninguna afirmación de conformidad ni de certificación para esta superficie.
  • Todas las clases declaran strict_types=1 y son final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy y DeadLetterEntry son final readonly con propiedades públicas promovidas.
  • El módulo lleva una anotación @since de 2.2.0; el esquema de firma vinculado a la marca de tiempo es un cambio con ruptura documentado en 3.1.0.
  • El motor de entrega toma abstracciones PSR-18/PSR-17, de modo que un cliente HTTP simulado ejercita sin conexión toda la ruta de envío, reintento y mensajes fallidos. El logger es nulo por defecto; inyectar un logger PSR-3 en producción o los fallos solo se manifestarán a través de los valores de retorno.
  • Las implementaciones del receptor deberían usar hash_equals() para la comparación de la firma e imponer una ventana de frescura sobre X-NextPDF-Timestamp.
  • Pruebas de límite recomendadas: registro con tenant no coincidente, difusión con lista de eventos vacía, 4xx terminal, agotamiento de reintentos, URL bloqueada por SSRF, rechazo de firma con marca de tiempo manipulada frente a un vector fijo y acotación del recuento de intentos en mensajes fallidos.

Esta página documenta únicamente el comportamiento observable externamente y la superficie de la API pública admitida. Las rutas internas de espacios de nombres, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbooks y los prefijos de tickets quedan fuera de alcance.