Un callback avisa a tu servidor cuando una ejecución ha terminado, de modo que no necesitas hacer polling ni mantener abierto un flujo de eventos. Ephemeral envía una solicitud POST firmada con el objeto de la ejecución cuando esta alcanza un estado final.
Solicitar un callback
Añade callback a la solicitud de ejecución.
callback.urlstringobligatoriocallback.secretstringobligatorio{
"workflow": "investigate-payment",
"input": { "payment_id": "pay_123" },
"max_cost_micros": 500000,
"callback": {
"url": "https://example.com/hooks/ephemeral",
"secret": "3f9a6c1e8b2d47f0a5c3e9d1b7f2a6c4"
}
}El secreto nunca se devuelve en ninguna lectura, ni siquiera en la definición de la ejecución. Los callbacks no forman parte de los flujos de trabajo guardados; configura uno en cada ejecución que lo necesite.
Qué recibes
El cuerpo es el objeto de la ejecución tal como está en el momento de la entrega, enviado como JSON con estas cabeceras:
| Cabecera | Valor |
|---|---|
X-Ephemeral-Run | El ID de la ejecución. |
X-Ephemeral-Timestamp | Hora Unix en segundos en la que se firmó este intento. |
X-Ephemeral-Signature | sha256= seguido del HMAC-SHA256 en hexadecimal de la marca de tiempo, un punto y el cuerpo sin procesar, con tu secreto como clave. |
Content-Type | application/ |
<X-Ephemeral-Timestamp>.<raw request body>Verificar la firma
Calcula el HMAC sobre los bytes sin procesar del cuerpo antes de analizarlo y compara en tiempo constante.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/ephemeral", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Ephemeral-Timestamp") ?? "";
const signature = req.get("X-Ephemeral-Signature") ?? "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.EPHEMERAL_CALLBACK_SECRET)
.update(timestamp + ".")
.update(req.body)
.digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
if (!valid || !fresh) return res.sendStatus(401);
const run = JSON.parse(req.body);
// Record run.id first and skip it if you have already handled it.
res.sendStatus(204);
});Cada intento se firma con la hora actual, así que rechazar las marcas de tiempo antiguas limita los ataques de repetición sin rechazar los reintentos legítimos.
Entrega y reintentos
Cualquier respuesta 2xx cuenta como entregada. Cualquier otra, incluidos un tiempo de espera agotado o una redirección, se reintenta: primero a los 10 segundos y después duplicando la espera cada vez, hasta unos 43 minutos entre intentos. Las entregas pendientes se almacenan de forma duradera, así que un reinicio del motor no las pierde.
X-Ephemeral-Run para que el procesamiento sea idempotente.billing_pending es true en el cuerpo, vuelve a leer la ejecución más tarde con GET /v1/runs/{id} para obtener su coste final.