Webhooks & Events
How deployed Apps push results to your systems — the async callback delivery contract, receiver requirements, and the event surfaces you can stream or poll.
Dynamiq's outbound "webhook" mechanism is the async run callback: you attach up to five callback URLs to an individual request, and the App POSTs the result to each when the run finishes. There is no standing webhook subscription to configure on an App — delivery is always declared per request. This page is the receiver-side contract: exactly what arrives at your endpoint, the delivery guarantees, and the streaming/polling alternatives when callbacks aren't the right fit.
Event surfaces at a glance
| Surface | Direction | Mechanism |
|---|---|---|
| Async callbacks | Dynamiq → you | Per-request callbacks list; one POST per callback when the run finishes |
| Run event stream | you ← Dynamiq (pull) | SSE stream / paginated list of typed run events on /v1/runs/{run_id}/events |
| Trigger events log | external app → Dynamiq | Inbound events received by a trigger, inspectable in the UI and API |
Requesting callbacks
Send "execution_mode": "async" with a callbacks array on a normal App invocation. The App answers 202 immediately with a request ID:
curl -X POST "https://<your-app-hostname>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-d '{
"input": { "question": "Generate the weekly report" },
"execution_mode": "async",
"callbacks": [
{
"url": "https://hooks.example.com/dynamiq/results",
"auth": { "type": "bearer", "token": "'"$CALLBACK_SHARED_SECRET"'" },
"metadata": { "job_id": "job-841" }
}
]
}'{
"id": "f7c7bb61-4f9f-4fd0-940b-98ebd5bd2777",
"status": "accepted"
}Rules enforced at request time:
- Up to 5 callbacks per request;
callbacksis only allowed with"execution_mode": "async"(400otherwise). - Each
urlmust be HTTPS and publicly resolvable — URLs pointing at private, loopback, link-local, or other reserved IP ranges are rejected. authis optional and supports only"type": "bearer".metadatais an arbitrary object echoed back verbatim — use it to route the result inside your system.
Client code for all three languages is on Streaming & Async Jobs.
The callback request
When the run finishes, each callback URL receives one HTTP request:
- Method:
POST - Headers:
Content-Type: application/json,User-Agent: Dynamiq, and — if you setauth—Authorization: Bearer <your token> - Body:
idstringrequiredstatusstringrequiredtimestampstringrequiredoutputobjectmetadataobject{
"id": "f7c7bb61-4f9f-4fd0-940b-98ebd5bd2777",
"status": "succeeded",
"timestamp": "2026-03-26T08:34:27.337615881Z",
"output": {
"output": "Here is the weekly report..."
},
"metadata": {
"job_id": "job-841"
}
}Always check status before reading output.
Delivery semantics
Design your receiver around these properties:
- One attempt, no retries. Each callback is delivered exactly once per run completion. If your endpoint is down or times out, that delivery is lost — Dynamiq does not retry, and your response status is not inspected.
- Independent and concurrent. Multiple callbacks on one request are delivered in parallel; one failing doesn't affect the others.
- Redirects are not followed. The callback must be served directly at the URL you registered.
- SSRF-safe egress. Delivery re-resolves the hostname at send time and refuses private, loopback, link-local, CGNAT, and other reserved addresses — a callback host must stay publicly routable.
- Respond fast. Acknowledge with a
2xximmediately and process the payload asynchronously; the delivery client applies connection and response-header timeouts.
If you need guaranteed result retrieval, don't rely on callbacks alone. Start the run as a background run on the Runs API (POST /v1/runs with "background": true) and poll GET /v1/runs/{run_id} — the run record persists, so nothing is lost if your service was down. See The Runs API.
A minimal receiver
Verify the bearer token you registered, acknowledge, then process:
import hmac
import os
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SHARED_SECRET = os.environ["CALLBACK_SHARED_SECRET"]
@app.post("/dynamiq/results")
async def dynamiq_callback(request: Request, authorization: str = Header(default="")):
token = authorization.removeprefix("Bearer ")
if not hmac.compare_digest(token, SHARED_SECRET):
raise HTTPException(status_code=401)
body = await request.json()
if body["status"] == "succeeded":
print("Run", body["id"], "finished:", body.get("output"))
else:
print("Run", body["id"], "failed:", body.get("output"))
# job_id from the metadata you attached when invoking the App
print("Routing key:", body.get("metadata", {}).get("job_id"))
return {"ok": True}Run it with uvicorn main:app --port 8000 behind a public HTTPS endpoint.
import express from "express";
import { timingSafeEqual } from "node:crypto";
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.CALLBACK_SHARED_SECRET!;
const safeEqual = (a: string, b: string) => {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
return ab.length === bb.length && timingSafeEqual(ab, bb);
};
app.post("/dynamiq/results", (req, res) => {
const token = (req.headers.authorization ?? "").replace(/^Bearer /, "");
if (!safeEqual(token, SHARED_SECRET)) {
return res.status(401).end();
}
const { id, status, output, metadata } = req.body;
if (status === "succeeded") {
console.log("Run", id, "finished:", output);
} else {
console.log("Run", id, "failed:", output);
}
console.log("Routing key:", metadata?.job_id);
res.json({ ok: true });
});
app.listen(8000);Streaming and polling alternatives
Callbacks deliver only the final result. For everything in between, use the typed event surfaces:
- Run event stream —
POST /v1/runswith"stream": true(or re-attach withGET /v1/runs/{run_id}/stream) emitsrun.created, node progress, content deltas, human-feedback requests, and terminal events as SSE. Events are also listable after the fact withGET /v1/runs/{run_id}/events. See The Runs API. - Trigger events log — every event received by a Trigger (Slack messages, schedule fires, …) is recorded with its status (
accepted,skipped, orfailed), raw payload, received-at time, and — when a run was started — the run ID. Inspect it on the trigger's events page or viaGET /v1/apps/{app_id}/triggers/{trigger_id}/events.
Next steps
Triggers
Invoke a deployed App automatically on a cron schedule, at a one-off time, or when an event arrives from a connected app such as Slack or email.
Variables
Configuration values and secrets for deployments — environment variables on Service Deployments, and where workflow Apps get their configuration instead.