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.
Cómo funciona el registro
Sección titulada «Cómo funciona el registro»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
webhookUrlysecret. - Subida por URL — envía los campos JSON
callbackUrlyhmacSignature.
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"Eventos
Sección titulada «Eventos»type del evento | Se dispara cuando |
|---|---|
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. |
Los estados queued y quota_pending no se entregan como webhooks.
Payload
Sección titulada «Payload»El cuerpo de la petición es JSON. Content-Type: application/json.
{ "id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.completed", "apiVersion": "2026-06-05", "occurredAt": "2026-06-05T12: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-05). 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. completed, failed, canceled, quota_exceeded. |
Los campos nulos se omiten del JSON.
Cabeceras de entrega
Sección titulada «Cabeceras de entrega»| 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 |
Verificar la firma
Sección titulada «Verificar la firma»La cabecera X-Accessful-Signature tiene la forma t=<unix>,v1=<hex>. Recalcúlala y
compárala en tiempo constante:
- Lee
tyv1de la cabecera. - 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. - Compara en tiempo constante contra
v1. - Opcionalmente, rechaza si
ttiene 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;}String[] kv = signatureHeader.split(","); // ["t=...", "v1=..."]long t = Long.parseLong(kv[0].substring(2));String v1 = kv[1].substring(3);
Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] sig = mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8));String expected = HexFormat.of().formatHex(sig);
boolean valid = MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), v1.getBytes(StandardCharsets.UTF_8));Reintentos e idempotencia
Sección titulada «Reintentos e idempotencia»- É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.