> Source: https://builder.ema.ai/v2/builder-guides/triggers-webhook
> Title: Webhook Triggers

# Webhook Triggers

A webhook trigger gives you a URL. When another system sends a message to that URL, your AI Employee starts running — there's no clock to wait for and no check to come round. It's the only kind of trigger that reacts the moment something happens, which makes it the right choice whenever a delay actually costs something: a customer waiting on a reply, an incident that needs triaging now.

Setting one up is a two-sided job. Ema gives you a URL and a signing secret, and someone with access to the _other_ system puts them in and configures it to send. The part that takes real care is telling Ema how that system signs its messages — Ema can't discover that, so you'll want their webhook documentation open.

For help choosing between this and the other kinds of trigger, see [Starting an AI Employee Automatically](/builder/v2/builder-guides/automatic-ai-employee-triggers).

> [INFO]
> **Dashboard AI Employees only.** Webhook triggers are available on Dashboard AI Employees. See [Creating a Dashboard AI Employee](/builder/v2/builder-guides/dashboard-ai-employee).

## Before you start

Have these ready, because you'll need them mid-setup:

-   **Access to the sending system's webhook settings** — or a colleague who has it. You can't finish without pasting Ema's URL in over there.
-   **That system's webhook documentation.** You'll need to know how it signs its messages: which header carries the signature, and what exactly goes into it. This is the one thing you can't guess.
-   **A way to make a test event happen** in that system — create a dummy record, fire a manual test if it offers one.

If the sending system's docs mention a "signing secret", "webhook secret", or "signature header", you're in the right place. Some systems don't sign their messages as such, and send a fixed shared value instead — that's handled too, as **Plain token** in [Verification](#step-3-verification).

> [WARNING]
> **An AI Employee that other AI Employees call can't also have a webhook trigger.** The two are mutually exclusive in both directions — whichever you set up second is refused, and nothing is changed. If this AI Employee is called from elsewhere, remove the Agents that call it first.

## Step 1: Details

Give the trigger a name — it's how you'll recognize it in the list later. Then click **Create & continue**.

> [WARNING]
> **"Create & continue" really does create it.** That button mints your live URL and signing secret on the spot, and you can't step back to Details afterwards. If the AI Employee currently has manual input fields on its **Start**, you'll see a warning that creating the trigger replaces them — those fields won't be saved. Read that warning before continuing, because from here on the workflow's input comes from the webhook instead.

Leaving the wizard after this point leaves a real, switched-off trigger behind, with a live URL. That's harmless — but if you were only exploring, delete it rather than leaving it in the list.

## Step 2: Endpoint

This is the step you'll be copying out of.

**Your webhook URL** is shown here. Copy it into the sending system's webhook settings.

**The signing secret** is a value both sides know. The sending system uses it to stamp every message it sends; Ema uses the same value to check the stamp is genuine, which is how an event that didn't come from that system gets rejected. So what matters is that both ends hold the _same_ secret — not which end created it.

**Who provides it?** Two options, depending on the other system:

Option

Use it when

What you do

**Ema generates it**

The other system lets you paste in a secret of your choosing

Copy Ema's secret into that system

**App provides it**

The other system generates its own secret and shows it to you (Stripe, Slack, and most large products work this way)

Paste their secret into Ema and click **Save**

The secret is masked; use the eye icon to reveal it and the copy button to take it. You don't have to write it down anywhere — you can reveal it again later from the trigger's row in the list.

**You can change your mind later.** A trigger using an app-provided secret can be switched back with **Switch to Ema-generated secret** from its row, and the URL doesn't change. Ema asks you to confirm first, because the old secret stops working the moment you do it.

> [WARNING]
> **Sending the URL and secret to a colleague.** Setting a webhook up usually means handing both to whoever administers the other system. Treat the signing secret like a password — send it however your company shares credentials, not in a chat channel others can read or search. If it does end up somewhere it shouldn't, rotate it.

### Does this app verify the URL?

Leave this on **No** unless you know otherwise. Most systems accept a webhook URL the moment you paste it.

Some don't. Slack, Microsoft Graph and a few others send a one-time challenge to the URL first, and only accept it if the right value is echoed back. If yours does that, switch to **Yes**, describe the challenge, and Ema will answer it automatically. Then click **Reply to probe**, which opens a **5-minute window**, and trigger the URL check from the other system.

You'll see one of:

-   **"Received and responded"** — the check was answered. You're done.
-   **"A probe arrived but a challenge value was missing"** — Ema replied, but with the wrong thing. Fix the reply and verify again.
-   Nothing — no probe arrived within the window. Start it again and try once more.

If the check keeps failing, the reply is almost certainly the problem rather than the URL: a system expecting its challenge value back is never satisfied by a plain acknowledgement that doesn't contain it.

## Step 3: Verification

> [WARNING]
> **This step needs the sending system's signing details, and Ema can't discover them.** Everything here describes how that system builds its signature. A wrong guess means every event is rejected, so work from their webhook documentation rather than assumptions — then run the test at the end of this step to confirm it before anything goes live.

**Why this step exists.** Your webhook URL isn't a secret you can rely on — you paste it into another system's settings, and anyone who administers that system can see it. So "a message arrived at the right URL" proves nothing on its own: without verification, anyone holding the URL could start your AI Employee. This step is how Ema establishes that an event genuinely came from the system you set this up with.

Ema re-creates the signature for each incoming event and compares it to the one that arrived. If they don't match, the event is rejected and nothing runs.

### How is the signature built?

Four options: **HMAC-SHA256** (much the most common, and the default), **HMAC-SHA1**, **HMAC-SHA512**, and **Plain token**.

The three HMAC options behave identically apart from the hash. **Plain token** is different: there's no signature at all, and Ema just compares the value that arrived against the secret you set, character for character. Systems that send a fixed shared value — Microsoft Graph's `clientState`, Salesforce's organization id — use this.

### Where is the signature?

Choose **Header** (most common), **Query param**, or — for Plain token only — **Body field**. Then name it: the header name, the parameter name, or the path to the value in the body. This is always required.

**Prefix to strip** is for systems that label their signature, sending `sha256=abc123…` rather than `abc123…`. Put the label here and Ema removes it before comparing. It isn't part of what gets signed.

**Encoding** — Hex (default) or Base64. Their docs will say which.

### What's signed

This is where most misconfigurations happen. You're rebuilding, exactly, the string the other system runs through its signing function.

Start from one of the two presets:

-   **Body only** — just the request body. This is what most systems sign, and it's the default.
-   **Timestamp + body** — a timestamp header, a full stop, then the body. Common among systems that also guard against replays.

Then adjust by adding pieces. Six kinds are available: **Literal** (fixed text), **Header**, **Query**, **Body**, **Method**, and **URL**. They're combined left to right.

**A worked example.** Suppose the sending system's documentation says it signs

```text
v0:{timestamp}:{raw body}
```

…with the timestamp taken from an `X-Event-Timestamp` header. That's four pieces, in this order:

#

Piece

Set it to

1

**Literal**

`v0:`

2

**Header**

`X-Event-Timestamp`

3

**Literal**

`:`

4

**Body**

leave the path empty for the whole body

Then read the **Signs** preview underneath. It shows the string you've built — if it doesn't match their documentation character for character, the pieces are wrong or out of order.

**Working the builder.** **Start from** fills the builder with one of the presets; everything after that is manual. **\+ Add piece** appends a piece, and each one has **Move piece left** / **Move piece right** to reorder it and **Remove piece** to drop it — one piece always has to remain. A **Literal** takes text, a **Header** takes a header name, a **Query** takes a parameter name, and a **Body** takes a path, left empty for the whole body. **Method** and **URL** take no configuration.

> [WARNING]
> **The "Timestamp + body" preset guesses the header name.** It fills in a header called `X-Timestamp`, a full stop, and the body — but your app's timestamp header is almost certainly called something else. Replace that name with the one from its documentation, or every event fails to verify, looking exactly like a wrong secret.

> [DANGER]
> **The order matters, and getting it wrong looks exactly like using the wrong secret.** Both produce the same failure, with no way for Ema to tell you which it was — it only knows the signatures didn't match. If verification fails and you're confident about the secret, suspect the order and the pieces before anything else. Running a test (below) shows you the expected and received values side by side, which is the only way to tell them apart.

You won't find a "Timestamp" piece in the list, because a timestamp is just a Header, Query, or Body piece like any other. Add it as one, then switch on **Reject stale events** below to have Ema check its age.

If Ema notices two variable-length pieces sitting next to each other with nothing between them, it warns you but doesn't stop you — because whether that's wrong depends entirely on what the other system does. If they put a separator between those values, add a **Literal** to match.

### Reject stale events?

Off by default. Switching it on makes Ema drop events whose timestamp is older than an age you set, which stops somebody replaying a genuine old event back at you.

You'll choose where the timestamp lives, its name, and a maximum age in **seconds** — 300 by default, up to seven days. If you don't want an age check at all, leave the whole setting switched off rather than entering a very large number.

**The timestamp must be one of the signed pieces.** If it isn't, anyone could forge it, and the check is worthless — so Ema won't let you save that combination. If you see a coverage warning, add the timestamp to **What's signed** above.

### The reply Ema sends back

Also on this step: what Ema returns to the sending system for each accepted event. The status is always **200**. The body defaults to none, and you can optionally build a text, JSON, or XML reply — including values echoed from the incoming request, which some systems require.

Most setups need nothing here. Leave it as it is unless the other system's docs ask for a specific response.

### Test it

Optional, and worth doing every time.

Click **Run test**. That opens a **5-minute window**; go and make something happen in the other system. Ema captures the first event that arrives and shows you whether it verified — without starting a run.

Three possible verdicts:

-   **Signature verified** — you're good.
-   **Signature did not verify** — the usual causes, in order of likelihood: the wrong pieces or order in "What's signed", the wrong encoding (hex vs base64), or the wrong secret. The panel shows expected against received, which is how you tell them apart.
-   **Authentic but stale** — the signature is right, but the event is older than your freshness window. A real event this old would be rejected.

> [WARNING]
> **A passing test doesn't mean events will run yet.** The test reports only on the signature and freshness. It deliberately ignores whether the trigger is switched on and whether the AI Employee is published — so it can say "this would start 1 run" while a real event would be dropped. Both of those still have to be true. See [Step 5](#step-5-finish).

One more thing if you're editing an existing trigger rather than building a new one: a passing test describes your **draft**. Live traffic keeps using the published settings until you publish, so a test can pass while real events are still being rejected.

Changing any verification setting clears the test result, so you'll need to re-run it. That's intentional — a green tick from before your last edit doesn't mean anything.

## Step 4: Events

Two decisions: what counts as one event, and what makes an event unique.

### Item shape

-   **Single event** (default) — each message starts one run, and the whole body is the event.
-   **List: one run per item** — the message carries an array, and Ema starts one run per item in it.

For list mode you name the array's location, or leave it blank if the whole body is the array. If you've run a test, Ema checks the path against your real sample and tells you how many items it found.

> [WARNING]
> **An array arriving in Single mode is not split.** Fan-out only happens if you ask for it. A message containing forty records, on a Single-event trigger, produces one run holding all forty.

A few list-mode limits worth knowing: it works on JSON only, a delivery carrying more than **1000** items fails as a whole rather than partially running, and if the path doesn't point at an array the whole delivery fails too. An empty array is fine — it just produces no runs.

### What makes an event unique?

Systems retry. Without a way to recognize a repeat, a retried message would run your workflow twice. So Ema builds a key for each event and skips anything it has seen before.

Option

What it does

**Specific fields** (default, recommended)

You name the field(s) that identify an event — usually an id. Precise, and survives retries that reorder the body or add attempt-specific values.

**Whole event**

Hashes the entire body. Catches exact repeats only. Fine for simple cases.

**Don't deduplicate**

Every delivery starts a run, repeats included.

Then choose how long Ema remembers a key: **24 hours**, **7 days** (the default), **30 days**, or **Never** — meaning it never forgets. Match it to how long the sending system keeps retrying — while Ema still remembers the key a repeat is skipped, and one arriving after that counts as new and runs again.

> [WARNING]
> **Three ways this bites.** If a field you named is missing from an event, Ema falls back to hashing the whole body for that one — you'll see a "dedup fell back" note in the delivery log, and near-identical events can collapse together. If you're using list mode, don't key only on a header: headers are shared across the whole delivery, so every item would get the same key and collapse into one run. And changing this configuration later resets Ema's memory, so events it had already seen can run again.

## Step 5: Finish

A recap, and the important part: **two separate things have to be true before anything runs.**

1.  **Enable the trigger** — either **Enable now** here, or later from its row.
2.  **Publish the AI Employee.** Publishing is what actually starts runs. There's no separate go-live switch.

The trigger's state reflects both: **Off** → **On, not firing** (enabled, but nothing published) → **Active**.

> [DANGER]
> **A switched-off trigger still reports success.** Even while off, Ema acknowledges every event and answers the sending system's checks, so the URL looks perfectly healthy from that system's point of view. It sees success and won't retry — but nothing runs, and those events are gone for good. If you're expecting runs and getting none, check that the trigger is enabled _and_ the AI Employee is published before you go looking for a fault on the sending side.

## What your workflow receives

Creating the trigger replaced the manual input fields on **Start** with a single packet, delivered in the same shape by every kind of trigger. Your workflow reads values out of that packet instead of out of fields a person filled in.

Reference it as

What's in it

`trigger_input.body`

What the other system actually sent — the content you care about. Its shape is entirely theirs, so it looks however their documentation says it looks.

`trigger_input.headers`

The event's headers and metadata, also shaped by the sender.

`trigger_input.id`

A unique id Ema assigns this event.

`trigger_input.name`

The trigger's own name.

`trigger_input.received_at`

When Ema received it.

`trigger_input.trigger_id`

Which trigger fired — useful if the AI Employee has more than one.

So if the ticket id you need arrives inside the body as `ticket.id`, your workflow refers to it as `trigger_input.body.ticket.id`. In list mode, each run gets one item of the list rather than the whole delivery.

**Don't guess the shape — capture it.** Run a test on the Verification step and look at the captured event. That shows you exactly what arrives from this sender, which is faster and more reliable than working it out from their documentation.

## Who the runs run as

Webhook runs are performed with one person's access, so the run is checked against their permissions. It starts out as whoever created the trigger, and administrators can reassign it from the trigger's row; the change applies from the next event, with no republishing needed.

**Reassigning is how you hand a webhook over.** If that person leaves, the trigger shows that its **Runs as** user is no longer available and an administrator points it at someone else. Nothing is rebuilt, and the URL and secret stay exactly as they are — so the sending system never needs to know.

## The signing secret, later

You can reveal the secret again from the trigger's row at any time. Only the person who created the trigger, or an administrator, is allowed to — and even the creator is refused if the trigger runs as someone with broader access than they have. If a reveal is declined and you think it shouldn't be, that's usually why; ask an administrator.

**Rotating** replaces it — and the old secret stops working **immediately**, so every event fails until you've pasted the new one into the sending system. Have that system's settings open before you rotate, not after.

## Monitoring deliveries

Every webhook keeps a **delivery log**: each message received, when, how many items it became, what happened, and a link to the run.

Common outcomes and what they mean:

What you see

What happened

**Processed**

Accepted and handled.

**Skipped: trigger off**

The trigger is disabled. Ema still replied 200.

**Skipped: not published**

The AI Employee has no published version, or unpublished changes.

**Skipped: too old**

Older than your freshness window.

**Skipped: feature off**

Despite the wording, this means the **AI Employee is paused**. Check whether it's been disabled.

**Failed: couldn't read items**

List mode, but the body wasn't a list at the path given — or held more than 1000 items.

**Dead lettered**

Ema retried five times and never got through. Retries are automatic, at widening intervals.

**Duplicate (skipped)**

Recognized as an event already handled.

The trigger also carries a health verdict — **Healthy**, **Failing**, **Dead-letters**, **Silent**, **Waiting**, **Off**, **Not firing**, or **Draft** — so you can spot a webhook that has quietly stopped receiving anything, or one that's receiving events and failing to get through.

> [WARNING]
> **Rejected events don't appear in the delivery log.** When a signature fails to verify, Ema keeps no record of the message itself — no log entry, no copy of the body — so a trigger whose secret is wrong looks exactly like a trigger receiving no traffic. What Ema does keep is a count of how many events failed and why, and that's what the health verdict runs on: it turns to **Failing** and points at the signing secret. If the sending system insists it's delivering and your log is empty, read the health verdict rather than trusting the empty log.

## Turning it off, and deleting

The switch on the row turns the trigger off while keeping the URL, the secret, and the settings — the right choice for a pause. Ema keeps acknowledging events; it just doesn't run anything.

**Deleting** stops the URL working immediately and drops any queued events. Runs that already happened aren't affected. If it's the last trigger on the AI Employee, its **Start** reverts to manual input fields, and the fields it had before are not restored.

## Troubleshooting

**Nothing arrives at all.** Check the URL in the sending system character for character. Then check whether anything sits in front of Ema that might redirect it — a redirect drops the method and the body, so the event never really arrives.

**Every event is rejected.** In order: the pieces and their order in "What's signed", the encoding, then the secret. Run a test and compare expected against received.

**Events are accepted but nothing runs.** The trigger is off, or the AI Employee isn't published, or has unpublished changes. All three show up in the delivery log as a "Skipped" outcome.

**It ran once and then stopped.** Look for **Duplicate (skipped)** — if the sending system reuses the same id, everything after the first is treated as a repeat. Adjust what makes an event unique.

**One run instead of many.** Item shape is set to Single event. Switch to list mode and name the array.

**A large event never arrives.** Ema reads at most **1 MiB** (1,048,576 bytes) of an incoming message, and refuses anything bigger before reading it — so it never reaches your workflow and leaves **no delivery-log entry**. The evidence is on the other side: that system's own delivery history shows the attempt failing with an HTTP 413 response. If your events carry attachments, have the sending system send a link or an id instead, and let the workflow fetch the file itself.

## What's next

-   [Starting an AI Employee Automatically](/builder/v2/builder-guides/automatic-ai-employee-triggers) — choosing between the three kinds of trigger.
-   [Poll triggers](/builder/v2/builder-guides/triggers-poll) — for systems that can't send webhooks at all.
-   [Build your first workflow](/builder/v2/builder-guides/build-your-first-workflow) — publishing, without which no webhook fires.
-   [Launching and Monitoring](/builder/v2/testing-operations/launching-and-monitoring) — where runs and their history live.
