Un callback prévient votre serveur qu'une exécution est terminée : vous n'avez pas besoin d'interroger l'API ni de garder un flux d'événements ouvert. Ephemeral envoie une requête POST signée avec l'objet d'exécution une fois que l'exécution a atteint un statut final.
Demander un callback
Ajoutez callback à la requête d'exécution.
callback.urlstringobligatoirecallback.secretstringobligatoire{
"workflow": "investigate-payment",
"input": { "payment_id": "pay_123" },
"max_cost_micros": 500000,
"callback": {
"url": "https://example.com/hooks/ephemeral",
"secret": "3f9a6c1e8b2d47f0a5c3e9d1b7f2a6c4"
}
}Le secret n'est jamais renvoyé par aucune lecture, y compris la définition de l'exécution. Les callbacks ne font pas partie des workflows enregistrés ; définissez-en un sur chaque exécution qui en a besoin.
Ce que vous recevez
Le corps est l'objet d'exécution tel qu'il est au moment de l'envoi, au format JSON, avec ces en-têtes :
| En-tête | Valeur |
|---|---|
X-Ephemeral-Run | L'ID de l'exécution. |
X-Ephemeral-Timestamp | Heure Unix, en secondes, à laquelle cette tentative a été signée. |
X-Ephemeral-Signature | sha256= suivi du HMAC-SHA256 hexadécimal de l'horodatage, d'un point et du corps brut, avec votre secret comme clé. |
Content-Type | application/ |
<X-Ephemeral-Timestamp>.<raw request body>Vérifier la signature
Calculez le HMAC sur les octets bruts du corps avant de l'analyser, et comparez en temps constant.
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);
// Enregistrez d'abord run.id et ignorez-le si vous l'avez déjà traité.
res.sendStatus(204);
});Chaque tentative est signée avec l'heure courante : rejeter les horodatages anciens limite donc le rejeu sans rejeter les nouvelles tentatives légitimes.
Envoi et nouvelles tentatives
Toute réponse 2xx compte comme une livraison réussie. Tout le reste, y compris un délai dépassé ou une redirection, donne lieu à une nouvelle tentative : d'abord après 10 secondes, puis avec un délai doublé à chaque fois, jusqu'à environ 43 minutes entre deux tentatives. Les envois en attente sont stockés durablement : un redémarrage du moteur ne les perd donc pas.
X-Ephemeral-Run pour rendre le traitement idempotent.billing_pending vaut true dans le corps, relisez l'exécution plus tard avec GET /v1/runs/{id} pour connaître son coût final.