Intégralo en tu app

Callbacks de resultados

Los webhooks reducen la latencia entre que una tarea termina y tu app se entera. Son una optimización de entrega: tres intentos y luego el gateway deja de intentarlo. El endpoint de estado sigue siendo la fuente de verdad.

#Registra el callback al enviar la tarea

Pasa webhook_url en la solicitud de envío. Añade webhook_secret y cada entrega irá firmada — sin él, ninguna lo está.

submit.sh
curl https://www.aisplice.store/v1/videos \
  -H "Authorization: Bearer sk-YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: video-job-8842" \
  -d '{
    "model": "doubao-seedance-2.5",
    "prompt": "A cinematic product scene",
    "webhook_url": "https://your.app/hooks/ai-tasks",
    "webhook_secret": "whsec_replace_me"
  }'

Debe ser enrutable públicamente

localhost, 127.0.0.1, los rangos privados, las direcciones link-local y las de metadatos de la nube se rechazan al enviar la tarea — no en el momento de la entrega. Usa un túnel durante el desarrollo.

http o https, resoluble en el momento del envío

Se resuelve el hostname y se comprueba cada dirección devuelta. Un nombre que no se resuelve hace fallar el envío.

La entrega es de mejor esfuerzo

Tres reintentos; después el gateway se detiene y solo lo registra. El endpoint de estado sigue siendo la fuente de verdad.

localhost se rechaza al enviar la tarea

El host del callback se resuelve y se comprueba antes de aceptar la tarea, así que una dirección privada o de loopback hace fallar el propio envío — no la entrega una hora después. Durante el desarrollo, apunta webhook_url a un túnel público, u omítelo y consulta.

#Encabezados y payload

EncabezadoCuándo se envíaQué hacer con él
X-Xiaoxiang-SignatureSolo cuando la tarea se envió con webhook_secret.sha256=<HMAC-SHA256 en hex del cuerpo sin procesar>. Verifícalo antes de parsear, con una comparación de tiempo constante.
X-Xiaoxiang-EventEn cada entrega.task.completed o task.failed — enruta según este valor en lugar de deducir la intención a partir del payload.
X-Xiaoxiang-Event-IdEn cada entrega.Estable entre reintentos del mismo evento. Guárdalo y descarta los repetidos — es tu clave de deduplicación.
CampoTipoPresenteDescripción
event_idstringSiempreCoincide con X-Xiaoxiang-Event-Id. Deduplica según este valor.
eventstringSiempre"task.completed" o "task.failed".
task_idstringSiempreEl id que devuelve la llamada de envío — tu clave para relacionar registros.
statusstringSiempreEstado final en el vocabulario de video — "completed" o "failed", incluso en tareas de imagen y música.
modelstringCuando se conoceModelo que generó la tarea. Se omite si se desconoce.
urlstringSi tiene éxitoURL del resultado cuando la tarea tuvo éxito. Prefiere /content para obtener un enlace estable de la plataforma.
fail_reasonstringSi fallaSolo presente en caso de fallo.
quotaintegerSiempreCuota realmente consumida, en unidades internas. Concilia con el registro de uso, no con este campo.
created_atintegerSiempreSegundos Unix, envío de la tarea.
finished_atintegerSiempreSegundos Unix, paso a estado final.
delivery.json
{
  "event_id": "task_8f21c...:task.completed",
  "event": "task.completed",
  "task_id": "8f21c0d2-...",
  "status": "completed",
  "model": "doubao-seedance-2.5",
  "url": "https://cdn.example.com/renders/8f21c0d2.mp4",
  "quota": 128000,
  "created_at": 1789000000,
  "finished_at": 1789000184
}

#Verifica el cuerpo de la solicitud sin procesar

Calcula el HMAC-SHA256 sobre los bytes exactos que recibiste, antes de parsear el JSON — volver a serializar el payload cambia los bytes y rompe la comparación. Compara en tiempo constante y rechaza si no coincide.

import crypto from "node:crypto";

// Express: express.raw({ type: "application/json" }) — not express.json().
export function handleTaskWebhook(req, res) {
  const received = req.get("X-Xiaoxiang-Signature") ?? "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body)              // Buffer of the raw body
      .digest("hex");

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // Deduplicate first, acknowledge fast, then do the work off-request.
  if (!seen.add(req.get("X-Xiaoxiang-Event-Id"))) return res.status(200).end();
  queue.push(event);
  res.status(200).end();
}

#Entrega y deduplicación

Responde en 15 segundos

Ese es el timeout de entrega. Cualquier respuesta más lenta cuenta como intento fallido, así que encola el trabajo y devuelve primero un 2xx.

Deduplica por el id del evento

X-Xiaoxiang-Event-Id es estable entre reintentos. Guárdalo y descarta los repetidos antes de tocar el estado de negocio.

Espera tres reintentos y luego silencio

Las entregas fallidas se reintentan a los 5 s, 30 s y 2 min. Después, el gateway solo lo registra — no llegará nada más.

#Mantén un proceso de consulta para conciliar

La entrega de webhooks es de mejor esfuerzo

Ejecuta un proceso de baja frecuencia sobre las tareas que siguen sin estado final en tu base de datos — basta un despliegue durante una ventana de entrega para perder una. Consulta el endpoint de estado, corrige la fila y trata el webhook solo como una mejora de latencia.

¿Sigues con el problema?

Envíanos el ID de solicitud o el mensaje de error y lo revisaremos.