API reference

Callbacks

Signed completion callbacks.

A callback tells your server when a run has finished, so you do not need to poll or hold an event stream open. Ephemeral sends one signed POST request with the run object after the run reaches a final status.

Request a callback

Add callback to the run request.

callback.urlstringrequired
Your endpoint. It must use HTTPS and resolve to a public address. Redirects are not followed.
callback.secretstringrequired
24 to 1,024 characters, used to sign each delivery. Generate a random value and store it on your server.
JSON
{
  "workflow": "investigate-payment",
  "input": { "payment_id": "pay_123" },
  "max_cost_micros": 500000,
  "callback": {
    "url": "https://example.com/hooks/ephemeral",
    "secret": "3f9a6c1e8b2d47f0a5c3e9d1b7f2a6c4"
  }
}

The secret is never returned by any read, including the run definition. Callbacks are not part of saved workflows; set one on each run that needs it.

What you receive

The body is the run object as it is at delivery time, sent as JSON with these headers:

HeaderValue
X-Ephemeral-RunThe run ID.
X-Ephemeral-TimestampUnix time in seconds when this attempt was signed.
X-Ephemeral-Signaturesha256= followed by the hex HMAC-SHA256 of the timestamp, a period and the raw body, keyed with your secret.
Content-Typeapplication/json
Signed content
<X-Ephemeral-Timestamp>.<raw request body>

Verify the signature

Compute the HMAC over the raw bytes of the body before parsing it, and compare in constant time.

Node.js with Express
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);
});

Each attempt is signed with the current time, so rejecting old timestamps limits replay without rejecting genuine retries.

Delivery and retries

Any 2xx response counts as delivered. Anything else, including a timeout or a redirect, is retried: first after 10 seconds, then with the delay doubling each time up to about 43 minutes between attempts. Pending deliveries are stored durably, so an engine restart does not lose them.

Deduplicate by run ID
Your endpoint can receive the same run more than once, for example when it processed a delivery but the response was lost. Use X-Ephemeral-Run to make handling idempotent.
A callback can arrive before billing has settled. If billing_pending is true in the body, read the run again later with GET /v1/runs/{id} for its final cost.
© 2026 EphemeralProduced by erni.works
esc

Nothing matches “”.