Ir al contenido

Webhooks

En lugar de sondear, puedes hacer que Accessful envíe por POST un evento firmado a tu servidor en cuanto un trabajo cambie de estado.

No hay panel de control ni endpoint de registro. Adjuntas un callback por cada subida y tú eliges el secreto de firma — Accessful no emite ninguno.

  • Subida multipart — envía los campos de formulario webhookUrl y secret.
  • Subida por URL — envía los campos JSON callbackUrl y hmacSignature.
Terminal window
curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload" \
-H "X-API-Key: $ACCESSFUL_API_KEY" \
-F "files=@documento.pdf" \
-F "webhookUrl=https://tu-app.example.com/hooks/accessful" \
-F "secret=$TU_WEBHOOK_SECRET"
type del evento Se dispara cuando
case.queued El PDF fue aceptado y entró en la cola de análisis. Se dispara una vez y lleva la posición en la cola inicial.
case.running El trabajo empezó a procesarse. Puede dispararse más de una vez (una por iteración).
case.completed El resultado PDF/UA está listo para descargar.
case.failed El procesamiento falló.
case.canceled El caso fue cancelado.
case.quota_exceeded Rechazado porque la cuota del contrato está agotada.

El estado quota_pending no se entrega como webhook.

El cuerpo de la petición es JSON. Content-Type: application/json.

{
"id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40",
"type": "case.completed",
"apiVersion": "2026-06-26",
"occurredAt": "2026-06-26T12:34:56Z",
"data": {
"caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40",
"fileName": "documento.pdf",
"jobStatus": "completed"
}
}
Campo Tipo Notas
id UUID ID del evento. Úsalo como clave de idempotencia (ver abajo).
type string Uno de los tipos de evento anteriores.
apiVersion string Versión del contrato (2026-06-26). Fíjala para detectar cambios.
occurredAt ISO-8601 Cuándo se creó el evento; estable entre reintentos.
data.caseId UUID El caso al que se refiere este evento.
data.fileName string El nombre del archivo.
data.jobStatus string Estado bruto del trabajo, p. ej. queued, completed, failed, canceled, quota_exceeded.
data.queuePosition integer solo case.queued — rango inicial en la cola, empezando en 1.
data.queueTotal integer solo case.queued — número total de casos en espera en ese momento.
data.estimatedWaitSeconds integer solo case.queued — estimación aproximada de la espera en segundos.

Los campos nulos se omiten del JSON.

El evento case.queued es la contraparte basada en push de la posición en la cola de job-status: se dispara una vez, en el momento en que se acepta un PDF, y lleva la posición inicial para que puedas mostrar «eres el 7.º de la fila, ~5 min» sin sondear.

{
"id": "a3b1c2d4-0000-4a1e-8f3c-2d6b5a9e1c40",
"type": "case.queued",
"apiVersion": "2026-06-26",
"occurredAt": "2026-06-26T12:30:00Z",
"data": {
"caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40",
"fileName": "documento.pdf",
"jobStatus": "queued",
"queuePosition": 7,
"queueTotal": 23,
"estimatedWaitSeconds": 300
}
}

Este es un evento discreto del ciclo de vida, no un flujo en tiempo real — la posición es una instantánea en el momento de la aceptación y no se vuelve a enviar a medida que avanza la cola. Para una posición que se actualiza continuamente, sondea GET /job-status/{caseId}, cuya respuesta lleva los mismos campos queue* mientras el caso espera. Los campos queue* aparecen solo en case.queued; cualquier otro evento los omite.

Cabecera Ejemplo Propósito
X-Accessful-Signature t=1749126896,v1=9f86d0… Firma HMAC — verifica esta
X-Accessful-Webhook-Timestamp 1749126896 Segundos Unix; igual que t= arriba
X-Accessful-Event-Id f1d2c3b4-… Igual que id; clave de idempotencia
X-Accessful-Event-Type case.completed Enrutado sin analizar el cuerpo
X-Accessful-Case-Id 7c2f1e4a-… El ID del caso
X-Accessful-Delivery-Attempt 1 Contador de intentos, empieza en 1
X-Signature n4bQgY… legacy HMAC en base64 solo sobre el cuerpo

La cabecera X-Accessful-Signature tiene la forma t=<unix>,v1=<hex>. Recalcúlala y compárala en tiempo constante:

  1. Lee t y v1 de la cabecera.
  2. Calcula HMAC-SHA256(secret, "<t>.<cuerpo bruto>") y codifícalo en hexadecimal. La cadena firmada es el timestamp, un punto literal y luego el cuerpo bruto exacto de la petición — verifícalo antes de analizar el JSON.
  3. Compara en tiempo constante contra v1.
  4. Opcionalmente, rechaza si t tiene más de unos minutos (protección frente a repetición).
import crypto from 'node:crypto';
// `rawBody` deben ser los bytes exactos recibidos (p. ej. express.raw()).
function verify(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min
return valid && fresh;
}
  • Éxito = cualquier respuesta 2xx dentro del tiempo límite (5s para conectar, 10s para responder). Responde rápido y haz tu trabajo de forma asíncrona.
  • Fallo (no-2xx, timeout o error de conexión) se reintenta hasta 10 veces con retroceso exponencial y jitter — aproximadamente 10s → 30s → 1,5m → 4,5m → 13,5m → 40m → 2h, luego limitado a 6h, menos hasta un 20% de jitter. La ventana completa abarca varias horas.
  • Tras 10 intentos fallidos, el evento se abandona. Contacta con soporte para reenviarlo.
  • Idempotencia: el mismo evento puede llegar más de una vez (p. ej. un reintento después de que tu endpoint ya respondiera con éxito). Deduplica por X-Accessful-Event-Id — es estable en cada reintento.