Skip to content

Start building

Jump toHow it works

Webhooks

Webhooks, both ways

Start a workflow from any system with a signed request, and get signed events back when something happens in your organization. Both directions use a shared secret, so each side knows the other is who it says.

How it works

A webhook trigger belongs to a workflow. When the workflow is published, the trigger gets an address and a secret. A request to that address is checked, stored and answered at once with 202; the workflow runs right after, never inside the request.

  1. 01The sender posts

    Any body, signed with the trigger's secret.

  2. 02MyDeepAgents checks

    The signature, the size and whether it has seen this event before.

  3. 03The workflow runs

    With the payload as its input, shaped by your rules if you set any.

  • Webhooks start workflows. To have an agent answer a webhook, put an Agent step in the workflow.
  • The other direction: add a webhook endpoint and MyDeepAgents posts signed events to it when runs finish, approvals wait, connections change and more.

Add a webhook trigger

In the workflow builder, open the workflow's When card and choose Webhook. Or add it to the definition yourself:

Publish the workflow. Publishing gives the trigger its address and creates its secret. Each alias has its own address and secret, so the staging and live versions never share one; events sent to a staging address are marked as tests.

"triggers": [ … ]
JSON
{
  "id": "incoming",
  "type": "webhook"
}

Find the address and secret

Select the trigger in the builder: its panel shows the address. Publishing also creates a secret but never shows it, so choose Show a new secret in the panel to get one you can give the sender. Through the API, GET /v1/workflows/{id}/triggers lists each trigger with its url, and POST /v1/triggers/{id}/secret makes a new secret. The address stays the same when you publish again.

Text
https://api.mydeepagents.com/v1/in/k7m2p9qa/trg_01jcte7b16k06c684ac89xccgx

Send an event

Send a POST to the trigger's address. There is no API key: the signature is the proof.

POST /v1/in/{route_code}/{trigger_id}
BODY='{"order_id":"A-1001","amount":42}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MDA_WEBHOOK_SECRET" | sed 's/^.* //')

curl -X POST "https://api.mydeepagents.com/v1/in/k7m2p9qa/trg_01jcte7b16k06c684ac89xccgx" \
  -H "Content-Type: application/json" \
  -H "X-Signature: sha256=$SIGNATURE" \
  -d "$BODY"

How the body is read

FieldWhat it means
JSONRead as JSON. A JSON object becomes the run's input unless you map it.
Formapplication/x-www-form-urlencoded becomes a flat object; a repeated name keeps its last value.
Anything elseKept as text, available to your mapping rules.
EmptyTreated as an empty object.

A body can be up to 64 KB. Larger requests are refused with 413.

Verify with signatures

The default check is an HMAC-SHA256 of the raw body, keyed with the trigger's secret, sent in the X-Signature header. MyDeepAgents computes the same value and compares the two in constant time.

The header

X-Signature: sha256= followed by the hex digest. The sha256= prefix is optional. You can choose another header name, and base64 instead of hex.

  • Sign the exact bytes you send. Re-serializing the JSON, even with different spacing, changes the signature.
  • The HMAC key is the secret exactly as shown, as text. Don't decode it.

Add a timestamp against replays

Name a timestamp header in the trigger, such as X-Timestamp, and the signed text becomes the timestamp, a dot, then the raw body. The timestamp is Unix seconds and must be within 5 minutes of the time it arrives, so a captured request can't be replayed later.

JSON
{
  "id": "incoming",
  "type": "webhook",
  "verify": {
    "scheme": "hmac_sha256",
    "header": "X-Signature",
    "encoding": "hex",
    "timestamp_header": "X-Timestamp"
  },
  "dedupe": {
    "from": "header",
    "name": "X-Event-Id"
  },
  "headers": [
    "X-Event-Id"
  ]
}

Sign a request

BODY='{"order_id":"A-1001","amount":42}'
TS=$(date +%s)
SIGNATURE=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$MDA_WEBHOOK_SECRET" | sed 's/^.* //')

curl -X POST "https://api.mydeepagents.com/v1/in/k7m2p9qa/trg_01jcte7b16k06c684ac89xccgx" \
  -H "Content-Type: application/json" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: sha256=$SIGNATURE" \
  -d "$BODY"

The simpler alternative: a static token

For senders that can't compute a signature, the static_token check compares a header with the secret exactly. It proves less, since anyone who sees one request can reuse it, so organizations under the privacy or HIPAA profiles can't publish it.

Signature tester

Compute a signature here to check your code against. Everything runs in your browser; nothing is sent anywhere.

The trigger's secret, as shown when it was created.

Exactly the bytes you will send.

Encoding
Text that is signed
{"order_id":"A-1001","amount":42}

Enter a secret to see the signature.

Responses

The answer is short on purpose: a refusal never explains itself beyond its status, so it teaches nothing to someone probing your address.

StatusWhat it means
202Received. The body holds the event's ID. A repeat of an event already received gets the first event's ID. {"received": true, "event_id": "evt_…"}
401The signature or timestamp is missing or wrong.
404No such trigger at this address.
410The trigger was retired, or the organization is not active.
413The body is over 64 KB.

Shape the payload

By default the JSON object you send becomes the workflow's input. To use only part of it, rename fields or skip some events, add a filter and a map to the trigger. Both are expressions over event:

eventWhat it means
event.idThe event's ID.
event.bodyThe parsed body.
event.headersThe headers you chose to keep, plus content-type and user-agent.
event.queryThe query string, as an object.
event.received_atWhen it arrived.

Start only for paid orders, with two fields

  • filter must be true for the workflow to start; otherwise the event is logged as filtered.
  • map builds the input. An expression on its own keeps its type, so a number stays a number.
  • The input is then checked against the workflow's input schema. If it doesn't fit, the event is logged as map_error and no run starts.
JSON
{
  "id": "incoming",
  "type": "webhook",
  "filter": "event.body.status == 'paid'",
  "map": {
    "order_id": "${event.body.order.id}",
    "total": "${event.body.order.total}"
  }
}

Duplicates

Senders retry, so the same event can arrive twice. MyDeepAgents remembers each event for 24 hours and answers a repeat with 202 and the first event's ID, without starting another run.

dedupe.fromWhat it means
hashBy default, two requests are the same event when their bodies are identical.
headerUse a header instead, such as an event ID the sender puts in every request.
bodyOr a field in the body, by its dotted path, like data.id.

Rotate the secret

Rotate a secret when it may have leaked, or on a schedule. The new secret works at once, and the old one keeps working for 24 hours, so you can update the sender without losing events.

POST /v1/triggers/{trigger_id}/secret
curl -X POST "https://api.mydeepagents.com/v1/triggers/trg_01jcte7b16k06c684ac89xccgx/secret" \
  -H "Authorization: Bearer $MDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

To use a secret the sender gave you instead, send it in the body (16 to 512 characters).

Test and monitor

Send a test event without signing anything: it starts the workflow as a real event would, marked as a test. It works on any active webhook or schedule trigger.

POST /v1/triggers/{trigger_id}/test
curl -X POST "https://api.mydeepagents.com/v1/triggers/trg_01jcte7b16k06c684ac89xccgx/test" \
  -H "Authorization: Bearer $MDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "order_id": "A-1001",
      "amount": 42
    }
  }'

The event log

Each trigger keeps a log of its events, newest first: where each came from, whether it was a test, what happened, and the run it started. Payloads are not in the log. Events are kept for 30 days after their run finishes.

GET /v1/triggers/{trigger_id}/events
JSON
{
  "object": "list",
  "data": [
    {
      "id": "evt_01jc0yrbnm84d8zbtsv0h4ksah",
      "object": "trigger_event",
      "source": "webhook",
      "is_test": false,
      "status": "started",
      "status_reason": null,
      "run_id": "wfr_01jcvk7x45pq07bh867msccyhv",
      "created_at": "2026-11-10T09:30:00.000Z"
    },
    {
      "id": "evt_01jc0yrbnm84d8zbtsv0h4q7rd",
      "object": "trigger_event",
      "source": "webhook",
      "is_test": false,
      "status": "filtered",
      "status_reason": null,
      "run_id": null,
      "created_at": "2026-11-10T09:12:41.000Z"
    }
  ],
  "has_more": false
}
StatusWhat it means
receivedAccepted and waiting to start the workflow.
startedStarted a run.
filteredThe filter said no.
filter_errorThe filter could not be evaluated.
map_errorThe mapped input didn't fit the workflow's input schema.
dropped_runningSkipped because a run was already going and the trigger allows only one.
skipped_pausedArrived while the trigger was paused.

Trigger settings

Every setting of a webhook trigger, with its default. The builder sets the common ones; the rest go in the definition.

FieldValueWhat it means
id—The trigger's name inside the workflow. Lowercase letters, digits and underscores.
typewebhookAlways webhook.
verify.schemehmac_sha256hmac_sha256 or static_token.
verify.headerX-SignatureThe header that carries the signature or token.
verify.encodinghexhex or base64, for HMAC.
verify.timestamp_headernullTurns on timestamped signatures when set.
dedupe.fromhashhash, header or body.
dedupe.name · dedupe.pathnullThe header or dotted body path to de-duplicate on.
headers[]Extra headers to keep for your rules.
filternullAn expression that must be true to start a run.
map{}How to build the run's input from the event.

Receive events

Add a webhook endpoint, the address of a server you run, and choose the events it should hear about. MyDeepAgents then posts each event to it as JSON, signed with the endpoint's own secret.

Add an endpoint in Developers → Webhooks → Endpoints, or with POST /v1/webhook_endpoints. The address must be https on the internet. The signing secret, starting whsec_, is shown once, when the endpoint is made.

What arrives

The body is deliberately thin: IDs and states, never content such as messages or files. Read the details with your API key when you need them. This keeps customer data out of every log the event passes through.

POST https://example.com/hooks/mydeepagents
JSON
{
  "id": "evt_01jc0yrbnm84d8zbtsv0h4ksah",
  "type": "agent_run.state_changed",
  "created_at": "2026-11-10T09:30:00.000Z",
  "tenant_id": "ten_01jcs79j0bwr7ahcbyn0fjsmk6",
  "workspace_id": "ws_01jccj30nr8ad7mx1yc5v9zx3h",
  "data": {
    "object": "agent_run",
    "id": "run_01jcr77659p6hws1zjqkthay73",
    "run_id": "run_01jcr77659p6hws1zjqkthay73",
    "state": "completed"
  }
}

Headers

HeaderWhat it means
MDA-Signaturet= the Unix time it was signed, then v1= the signature. During a secret rotation there are two v1 values.
MDA-Event-IdThe event's ID. The same event can arrive more than once; use this to notice.
MDA-Delivery-IdThis delivery's ID, different for each redelivery.
MDA-Event-TypeThe event's type, so you can route it before parsing the body.
User-AgentAlways MyDeepAgents-Webhooks/1.

Verify an event

Check every request before you trust it. The signature is an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's secret exactly as shown, whsec_ included.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a Buffer), before JSON parsing.
export function verifyMdaSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const t = Number(parts.find(([key]) => key === "t")?.[1]);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  return parts
    .filter(([key]) => key === "v1")
    .some(([, value]) => {
      const given = Buffer.from(value, "hex");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
}

// Express: keep the raw body for this route.
app.post("/hooks/mydeepagents", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyMdaSignature(req.body, req.get("MDA-Signature") ?? "", process.env.MDA_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(204); // answer fast; do the work after
});
  • Use the raw bytes of the body as received, before any JSON parsing.
  • Refuse a timestamp more than 5 minutes from your clock, so a captured request can't be replayed.
  • Accept the request if any v1 matches, and compare in constant time.
  • Deliveries come at least once and in no particular order: skip an event ID you have already handled, and trust the state in the body over the order of arrival.

Retries and redelivery

If your server doesn't answer with a 2xx, MyDeepAgents tries again: after 10 seconds, then each wait three times longer, up to 4 hours apart, with a little randomness, for 24 hours in all. That is about 13 attempts.

After a day of failures the organization's admins are told the endpoint is failing. After three days it is turned off, and they are told again. Turning it back on sends a test event first and works only if your server answers.

Each endpoint's page lists every delivery for 30 days, with each attempt's answer and timing. From there you can send a test event, send any delivery again, pause the endpoint or rotate its secret; the old secret keeps signing for 24 hours so you can switch without missing events.

Event types

Subscribe an endpoint to every event, to all events about one thing (agent_run.*), or to the ones you list. New types can appear; an endpoint on every event gets them automatically. Test events have the type ping.

AboutEvents
agent_runagent_run.state_changed
workflow_runworkflow_run.state_changed
approvalapproval.requestedapproval.resolved
agentagent.publishedagent.alias.moved
workflowworkflow.publishedworkflow.alias.moved
widgetwidget.publishedwidget.alias.movedwidget.status.changed
connectionconnection.authorizedconnection.health_changed
mcp_servermcp_server.tools_approvedmcp_server.tool_changedmcp_server.health_changed
membermember.addedmember.removed
api_keyapi_key.createdapi_key.revoked
delegationdelegation.accepteddelegation.revoked
budgetbudget.threshold_crossed
key_configkey_config.health.changed
notificationnotification.created

Build with us

Ready for your first call?

Early access is open. Tell us what you want your agents to do, and we will help you get your first key.