Fallos de emisión masiva (DLQ)
Cuando una emisión asíncrona (vía POST /credentials/issue con 2+ recipients) deja filas sin emitir — por validación, error transitorio, etc. — esas filas quedan persistidas indefinidamente en una bandeja de fallos (Dead Letter Queue). Los siguientes endpoints permiten consultarlas, exportarlas y reintentarlas.
- Durante el job:
GET /jobs/{job_id}devuelve el conteofaileden vivo. - Hasta 7 días después del job:
GET /jobs/{job_id}/failuresresuelve eljob_idalbatch_uuidvía un mapeo Redis y lista los fallos. - Después de 7 días: el mapeo expira y el endpoint anterior responde
410 Gone. UsáGET /bulk-failures?batch_uuid=...(la tabla persiste indefinido). - La retención automática mueve fallos
pending_review > 90 díasadiscarded, y borradiscarded/retried > 365 días.
GET /jobs/{id}/failures
Lista los fallos asociados al job. Solo accesible mientras el mapeo Redis siga vivo (7 días tras el dispatch).
Request
GET /api/v1/jobs/{job_id}/failures
Headers
| Header | Requerido | Descripción |
|---|---|---|
X-API-Key | Sí | Tu API key |
Response (200)
{
"job_id": "abc123-task-id",
"batch_uuid": "550e8400-e29b-41d4-a716-446655440000",
"total_failures": 2,
"failures": [
{
"id": "9f2a4b7d-1c3e-4a5f-8b6c-2d3e4f5a6b7c",
"batch_uuid": "550e8400-e29b-41d4-a716-446655440000",
"achievement_id": "1f2a3b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
"recipient_email": "[email protected]",
"row_data": {
"email": "[email protected]",
"recipient_name": "Juan Perez",
"credits_earned": null
},
"error_category": "validation_error",
"error_message": "credits_earned es requerido",
"attempts_made": 1,
"first_seen_at": "2026-05-09T15:30:00Z",
"last_attempt_at": "2026-05-09T15:30:00Z",
"status": "pending_review",
"retried_credential_id": null
}
]
}
Errores
| Status | Detalle | Causa |
|---|---|---|
403 | Access denied to this job | El job fue creado con otro API key |
410 | Job too old; failures retention expired. Use GET /api/v1/bulk-failures with batch_uuid filter. | Pasaron más de 7 días desde el dispatch |
GET /bulk-failures
Auditoría histórica con paginación. Tenant-scoped: siempre devuelve solo fallos del tenant del API key.
Request
GET /api/v1/bulk-failures?page=1&page_size=20&status=pending_review
Query Parameters
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
page | int | 1 | Página, mínimo 1 |
page_size | int | 20 | Items por página, máximo 100 |
achievement_id | UUID | — | Filtra por credencial |
error_category | string | — | validation_error, signing_error, image_error, external_service_error, rds_error, unknown |
status | string | sin filtro | pending_review, retried, discarded, resolved |
batch_uuid | UUID | — | Filtra por envío específico (útil cuando expiró el mapeo de /jobs/{id}/failures) |
Response (200)
{
"page": 1,
"page_size": 20,
"total": 45,
"items": [
{ "id": "...", "batch_uuid": "...", "...": "..." }
]
}
Los campos de cada item son los mismos que en /jobs/{id}/failures.
POST /bulk-failures/{id}/retry
Reintenta una fila fallida. Genera un nuevo batch_uuid internamente para no chocar con la idempotency del envío original.
Request
POST /api/v1/bulk-failures/{failure_id}/retry
Content-Type: application/json
X-API-Key: uc_live_...
{
"row_data_overrides": {
"credits_earned": 5
}
}
Body
| Campo | Tipo | Notas |
|---|---|---|
row_data_overrides | object | null | Opcional. Mergea sobre la fila original solo para esta ejecución; la fila almacenada NO se modifica (audit trail). Los overrides aplicados quedan registrados en last_retry_overrides. |
Response (200) — éxito
{
"success": true,
"credential_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"error_category": null,
"error_message": null
}
Response (200) — fallo (status sigue pending_review)
{
"success": false,
"credential_id": null,
"error_category": "validation_error",
"error_message": "credits_earned es requerido"
}
Nota: una respuesta con success=false devuelve HTTP 200 (no 4xx) — el reintento se ejecutó pero no produjo una credencial. El failure se actualiza con el nuevo error y el admin/integrador puede reintentar de nuevo.
Errores
| Status | Detalle | Causa |
|---|---|---|
404 | Fallo no encontrado | El ID no existe o pertenece a otro tenant |
Estructura de un fallo
| Campo | Tipo | Descripción |
|---|---|---|
id | UUID | ID único del fallo |
batch_uuid | UUID | Lote original al que pertenecía la fila |
achievement_id | UUID | Credencial que se intentó emitir |
recipient_email | string | Email del destinatario |
row_data | object | Fila original (mismo shape que el recipient enviado a /credentials/issue). Inmutable: refleja lo que se envió originalmente. |
error_category | string | Una de: validation_error, signing_error, image_error, external_service_error, rds_error, unknown |
error_message | string | Mensaje del último intento |
attempts_made | int | Cuántas veces se procesó (incluye el intento original + retries) |
first_seen_at | ISO 8601 | Cuándo se registró por primera vez |
last_attempt_at | ISO 8601 | Último intento |
status | string | pending_review, retried, discarded, resolved |
retried_credential_id | UUID | null | Si status='retried', ID de la credencial creada en el último retry exitoso |
Categorías de error
| Categoría | Significado | Acción típica |
|---|---|---|
validation_error | Datos faltantes o inválidos en la fila (ej: campo requerido vacío) | Corregir datos vía row_data_overrides y reintentar |
signing_error | Falla armando el JSON-LD OB3 o firmando | Probablemente un bug; reportar |
external_service_error | Servicio externo (Redis, etc.) o error inesperado | Reintentar; si persiste, reportar |
rds_error | Base de datos transitoria | Reintentar |
image_error | Falla generando la imagen (no debería aparecer aquí en la versión actual) | Reportar |
unknown | No categorizado | Inspeccionar error_message |