Cargando tu cuenta

Webhooks

Los webhooks envían un POST HTTPS a tu servidor cuando termina una generación, así no necesitas consultar.

Añadir un endpoint

Añade endpoints en tu página de cuenta, en la sección Desarrolladores: una URL https en el puerto predeterminado, los eventos que quieras y, opcionalmente, una clave de API cuyas generaciones recibe. Obtienes un secreto de firma que empieza por whsec_ y se muestra una sola vez.

Eventos

EventoSe envía cuando
generation.succeededuna generación terminó y sus salidas están listas.
generation.faileduna generación falló; sus monedas se reembolsaron.
generation.canceledse canceló una generación en cola.

Solicitud

El cuerpo es JSON con el id del evento, su type, createdAt y data.generation: la generación tal como la devuelve GET /v1/generations/:id, con enlaces de descarga nuevos.

  • Inker-Signature: la firma: t= la hora Unix en segundos y v1= el HMAC-SHA256 en hexadecimal.
  • Inker-Event-Id: el id del evento, el mismo en cada reintento. Úsalo para ignorar duplicados.
  • Inker-Event-Type: el tipo de evento.
HTTP
POST /webhooks/inker HTTP/1.1
Content-Type: application/json
Inker-Signature: t=1791200000,v1=5f0c…e81a
Inker-Event-Id: EVENT_ID
Inker-Event-Type: generation.succeeded

{
  "id": "EVENT_ID",
  "type": "generation.succeeded",
  "createdAt": "2026-10-05T12:00:00.000Z",
  "data": {
    "generation": {
      "id": "…",
      "modelId": "…",
      "status": "succeeded",
      "coinsQuoted": 12,
      "coinsCharged": 12,
      "outputs": [{ "id": "…", "kind": "image", "url": "https://cdn.inker.si/…" }]
    }
  }
}

Verificar la firma

Calcula el HMAC-SHA256, en hexadecimal, de la marca de tiempo, un punto y el cuerpo sin procesar de la solicitud, con tu secreto de firma completo como clave. Compáralo en tiempo constante con cada valor v1 y rechaza una marca de tiempo a más de 300 segundos de tu reloj. Usa siempre los bytes exactos que recibiste, antes de analizar el JSON.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;

/**
 * header: the Inker-Signature header ("t=<unix seconds>,v1=<hex>").
 * rawBody: the request body exactly as received (string or Buffer), before JSON parsing.
 */
export function verifyInkerSignature(secret, header, rawBody, nowMs = Date.now()) {
  if (!header) return false;
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(',')) {
    const [name, value] = part.trim().split('=', 2);
    if (!value) continue;
    if (name === 't' && /^\d{1,12}$/.test(value)) timestamp = Number(value);
    if (name === 'v1' && /^[0-9a-f]{64}$/.test(value)) signatures.push(value);
  }
  if (timestamp === null || signatures.length === 0) return false;
  if (Math.abs(Math.floor(nowMs / 1000) - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  return signatures.some((signature) => timingSafeEqual(Buffer.from(signature, 'hex'), expected));
}
Node.js · Express
import express from 'express';

const app = express();
const seen = new Set(); // use your database in production

// express.raw keeps the exact bytes the signature covers.
app.post('/webhooks/inker', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyInkerSignature(
    process.env.INKER_WEBHOOK_SECRET,
    req.get('Inker-Signature'),
    req.body,
  );
  if (!ok) return res.status(400).send('bad signature');

  const eventId = req.get('Inker-Event-Id');
  if (seen.has(eventId)) return res.sendStatus(200); // a retry of an event you handled
  seen.add(eventId);

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.type === 'generation.succeeded') {
    // event.data.generation.outputs[].url: download soon, links are short-lived
  }
  res.sendStatus(200); // answer 2xx within 10 seconds; do slow work afterwards
});
Python
import hashlib
import hmac
import re
import time

TOLERANCE_SECONDS = 300


def verify_inker_signature(secret: str, header: str | None, raw_body: bytes, now: float | None = None) -> bool:
    """header: the Inker-Signature header; raw_body: the request body exactly as received."""
    if not header:
        return False
    timestamp = None
    signatures = []
    for part in header.split(","):
        name, _, value = part.strip().partition("=")
        if name == "t" and re.fullmatch(r"[0-9]{1,12}", value):
            timestamp = int(value)
        elif name == "v1" and re.fullmatch(r"[0-9a-f]{64}", value):
            signatures.append(value)
    if timestamp is None or not signatures:
        return False
    current = int(time.time() if now is None else now)
    if abs(current - timestamp) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(signature, expected) for signature in signatures)

Reintentos

Responde con cualquier estado 2xx en menos de 10 segundos. Si no, inker vuelve a intentarlo después de 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h y 12 h: 9 intentos durante aproximadamente un día; después la entrega se marca como fallida. Una entrega puede llegar más de una vez y fuera de orden, así que elimina duplicados por el id del evento.

Si un secreto puede haberse filtrado, rótalo desde tu página de cuenta: el anterior deja de funcionar al instante. Allí también aparecen las entregas recientes de cada endpoint, con sus códigos de estado y errores.