|
Documentación
Reservar una demoPlataforma
PlataformaMCPCLIAPI
Flujos de trabajo
GuíasRegistro de cambios

Bienvenida

  • Descripción general
  • Autenticación
  • Errores y códigos de estado
  • Firmas de webhook

Localización

  • Descripción general
  • Crear trabajos
  • Bloquear claves no traducibles
  • Hacer seguimiento de un grupo de trabajos
  • Obtener un trabajo
  • Listar trabajos
  • Entrega del webhook
  • Progreso en tiempo real (WebSocket)

Pipeline

  • Descripción general
  • Edición con IA previa a la localización
  • Revisión humana
  • evaluación de IA (posedición)
  • Reescritura para que suene natural
  • Comprobación de retrotraducción
  • Configurar la canalización
  • Supervisar ejecuciones del pipeline

Aprovisionamiento

  • Descripción general
  • Crear un trabajo de aprovisionamiento
  • Tipos de fuente
  • Qué extrae la IA
  • Entrega de webhooks
  • Progreso en tiempo real (WebSocket)

Síncrono

  • Localize
  • Recognize

Gestión del motor

  • Sugerencias del motor

Firmas de webhook

Cuando termina una tarea asíncrona, Lingo.dev no te obliga a consultarla por sondeo. Te devuelve la llamada: una POST al endpoint HTTPS que registraste como tu callbackUrl. Esa es la comodidad. También es la exposición: una URL pública acepta todo lo que internet le envía, y cualquiera que conozca la tuya puede POST un evento falso de "tarea completada" a tu handler.

Por eso la regla para cualquier callback es siempre la misma: verifica antes de confiar. Cada entrega incluye una firma calculada a partir de un secreto que solo tenéis tú y Lingo.dev. Vuélvela a calcular de tu lado, compárala en tiempo constante y una carga útil falsificada nunca llegará a tu lógica de negocio. Esta página es el único sitio donde se explica ese mecanismo. Tanto los callbacks de localización como los de aprovisionamiento lo usan sin cambios; esas páginas cubren la forma de sus propias cargas útiles y remiten aquí para la verificación.

En esta página

  • Las tres cabeceras
  • El secreto de firma
  • Verificar una firma
  • Por qué importa el cuerpo sin procesar
  • Rechazar repeticiones
  • Responder rápido, procesar después
  • Reintentos y backoff

Las tres cabeceras#

Lingo.dev sigue la especificación Standard Webhooks, un esquema abierto que implementan varios proveedores, así que verificas contra un contrato publicado en lugar de hacerlo contra una solución propietaria y poco convencional. Cada entrega incluye tres cabeceras:

CabeceraDescripción
webhook-idUn identificador único de la entrega.
webhook-timestampMarca de tiempo Unix, en segundos, de cuándo se envió la entrega.
webhook-signatureLa propia firma: v1,{base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))}

El contenido firmado son esas tres partes unidas por puntos: webhook-id, luego webhook-timestamp y después el cuerpo sin procesar de la solicitud, en ese orden exacto. Reconstruye esa cadena, aplícale HMAC-SHA256 con tu secreto, codifica el resultado en base64 y tendrás el valor con el que comparar.

La cabecera webhook-signature puede incluir más de una firma separada por espacios, cada una etiquetada con una versión del esquema (v1,...). Un verificador acepta la entrega si alguna de las firmas coincide. Recorrer la lista en lugar de leer un único valor es la forma más segura de analizar esta cabecera, por eso los ejemplos de abajo iteran sobre cada firma presente.

El secreto de firma#

El secreto se genera para tu organización la primera vez que envías una tarea con un callbackUrl. Lleva el prefijo whsec_, seguido de los bytes de la clave codificados en base64:

text
whsec_Mf9aQ7n...base64...key...bytes

Elimina el prefijo whsec_ y decodifica en base64 el resto para recuperar los bytes sin procesar de la clave; ese valor decodificado es la clave HMAC, no la cadena con prefijo. Firmar contra el texto literal whsec_... es el motivo más habitual por el que una implementación que parece correcta nunca coincide, así que decodifica primero.

Trata el secreto como una clave de API

El secreto de firma es lo que distingue un callback real de uno falsificado. Guárdalo en el servidor, fuera del control de versiones y fuera de cualquier bundle de cliente. Cualquiera que lo tenga puede firmar cargas útiles que tu handler aceptará. Consulta API Keys para ver cómo gestiona Lingo.dev las credenciales con alcance de organización.

Verificar una firma#

La verificación es una única función que colocas una vez delante de tu handler. Hace tres cosas: vuelve a calcular la firma esperada a partir del cuerpo sin procesar, la compara con la que ha llegado mediante una comprobación en tiempo constante y rechaza todo lo que no coincida antes de que se ejecute tu código. La misma función protege cada evento asíncrono que Lingo.dev te envía: finalizaciones de localización, finalizaciones de aprovisionamiento, cada tipo y cada superficie del producto.

javascript
import crypto from "node:crypto";

function verifyWebhook(payload, headers, secret) {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];

  // Reject timestamps outside a tolerance window (replay prevention)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    throw new Error("Webhook timestamp too old");
  }

  // Recompute the expected signature over id.timestamp.body
  const content = `${msgId}.${timestamp}.${payload}`;
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");
  const expected = crypto
    .createHmac("sha256", secretBytes)
    .update(content)
    .digest("base64");

  // A delivery may carry several signatures; accept if any matches
  for (const sig of signatures.split(" ")) {
    const [version, value] = sig.split(",", 2);
    if (version === "v1" && crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(value)
    )) {
      return JSON.parse(payload);
    }
  }

  throw new Error("Invalid webhook signature");
}

Compara con una función en tiempo constante, crypto.timingSafeEqual, hmac.compare_digest, no con ==. Una comparación de cadenas normal devuelve el resultado en cuanto dos bytes difieren, y esa diferencia de tiempo basta para filtrar la firma byte a byte. La comparación en tiempo constante cierra ese canal lateral, por eso ambos ejemplos de arriba la usan.

Por qué importa el cuerpo sin procesar#

Fíjate en que ambas funciones firman payload: el cuerpo exactamente tal y como llegó por la red, antes de cualquier parseo de JSON. Este es el detalle que más a menudo hace tropezar una integración que por lo demás es correcta, y conviene dejarlo claro justo donde suele dar problemas:

La firma se calcula sobre los bytes exactos que envió Lingo.dev. En cuanto parseas el cuerpo como un objeto y vuelves a serializarlo, puedes cambiar espacios en blanco, el orden de las claves o el formato numérico, y el HMAC recalculado deja de coincidir con una firma obtenida a partir de los bytes originales. La carga útil es idéntica en significado; los bytes no.

Verifica con el cuerpo sin procesar, no con el objeto parseado

Captura el cuerpo sin procesar de la solicitud antes de que tu framework lo parsee y pásale esos bytes al verificador. En Express, usa express.raw({ type: "application/json" }) en la ruta del webhook. En FastAPI, lee await request.body(). Parsea solo después de que la firma haya quedado validada: primero la verificación, después el parseo.

Rechazar repeticiones#

Una carga útil firmada válida que un atacante haya capturado puede reproducirse literalmente: la firma sigue siendo válida, porque nada en ella cambia entre la primera entrega y una copia enviada una hora más tarde. La cabecera webhook-timestamp es lo que limita esa ventana: registra cuándo se envió la entrega, para que tu verificador pueda rechazar cualquier cosa más antigua que la tolerancia que elijas. Los ejemplos de arriba usan cinco minutos.

Una comprobación de marca de tiempo detiene una repetición antigua: una copia capturada y reenviada después de superar tu tolerancia no pasa la comprobación de vigencia y nunca llega a tu handler.

Responder rápido, procesar después#

Una vez verificada una entrega, devuelve 200 de inmediato y luego haz el trabajo real —escrituras en base de datos, llamadas a servicios dependientes, invalidación de caché— después de responder.

javascript
app.post(
  "/webhooks/lingo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    let event;
    try {
      event = verifyWebhook(req.body.toString(), req.headers, process.env.LINGO_WEBHOOK_SECRET);
    } catch {
      return res.status(401).send("invalid signature");
    }

    // Acknowledge first, process after - never block the response on slow work
    res.status(200).send("ok");
    void handleEvent(event);
  }
);

El motivo es mecánico, no estilístico. Un handler lento mantiene abierta la conexión HTTP; si tarda lo suficiente como para agotar el tiempo de espera, la entrega se considera fallida y se reintenta, así que meter trabajo pesado en la ruta de respuesta convierte un evento en varios. Confirma rápido, entrega el trabajo a una cola o a una tarea en segundo plano, y un único evento seguirá siendo un único evento. Las formas de carga útil sobre las que haces switch dentro de handleEvent se documentan con cada producto: callbacks de localización y callbacks de aprovisionamiento.

Reintentos y backoff#

Habrá momentos en los que tu endpoint no esté disponible: un despliegue, un timeout, un bad gateway. Lingo.dev no descarta el evento cuando eso ocurre.

Si tu endpoint devuelve un estado distinto de 2xx o no está accesible, la entrega se reintenta con backoff exponencial a partir de 30 segundos, hasta 5 intentos. Tras el quinto intento, la entrega se marca como fallida y Lingo.dev deja de intentarlo, pero el resultado no se pierde. Sigue pudiéndose recuperar desde el registro de la tarea, así que un periodo de inactividad te cuesta un callback, nunca el resultado en sí. Ese registro de la tarea es tu red de seguridad: crea el webhook para el caso habitual y trata la tarea almacenada como la fuente de verdad a la que siempre puedes recurrir. Para una tarea de traducción, consúltala directamente.

Siguientes pasos#

Autenticación
Cómo las claves de API autentican cada solicitud a la API
Webhooks de localización
Las formas de las cargas útiles de translation.completed y translation.failed
Webhooks de aprovisionamiento
Cargas útiles de callback para tareas de aprovisionamiento del motor de IA

¿Te ha resultado útil esta página?

Max PrilutskiyMax Prilutskiy·Actualizado hace 12 días·7 min de lectura