Dynamiq
Deploy & Integrate

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

SurfaceDirectionMechanism
Async callbacksDynamiq → youPer-request callbacks list; one POST per callback when the run finishes
Run event streamyou ← Dynamiq (pull)SSE stream / paginated list of typed run events on /v1/runs/{run_id}/events
Trigger events logexternal app → DynamiqInbound 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; callbacks is only allowed with "execution_mode": "async" (400 otherwise).
  • Each url must be HTTPS and publicly resolvable — URLs pointing at private, loopback, link-local, or other reserved IP ranges are rejected.
  • auth is optional and supports only "type": "bearer".
  • metadata is 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 set authAuthorization: Bearer <your token>
  • Body:
idstringrequired
The request ID from the 202 response — correlate the result with your original request.
statusstringrequired
"succeeded" when the run returned a success status, "failed" otherwise.
timestampstringrequired
RFC 3339 time the result was recorded.
outputobject
The workflow output on success. On failure it may carry the error payload, or be omitted if the response body was not valid JSON.
metadataobject
The metadata object you attached to this callback, echoed back.
{
  "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 2xx immediately 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 streamPOST /v1/runs with "stream": true (or re-attach with GET /v1/runs/{run_id}/stream) emits run.created, node progress, content deltas, human-feedback requests, and terminal events as SSE. Events are also listable after the fact with GET /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, or failed), raw payload, received-at time, and — when a run was started — the run ID. Inspect it on the trigger's events page or via GET /v1/apps/{app_id}/triggers/{trigger_id}/events.

Next steps

On this page