*Workflows › Build*

# Incoming webhooks

> A webhook trigger gives the workflow a private address. Your system POSTs JSON to it with a secret, Filmo answers 202 straight away, and a run starts.

## Set it up

- Choose «Webhook» as the trigger. Under «Body», declare the type of the JSON you will send.
- In the trigger's side panel you will find «Endpoint» and «Secret», each with a copy button. They are shown in clear text, and they exist from the moment the workflow is created.
- Publish the workflow and switch it «Live». The address answers 404 until then, and answers 404 again if the workflow is paused or the trigger is changed to another type.

## The request

*A delivery (fictitious data)*

```bash
curl -sS -X POST "https://app.filmo.io/api/webhooks/workflows/whk_<token>" \
  -H "X-Webhook-Secret: whsec_<secret>" \
  -H "Idempotency-Key: order-2026-0042-reserved" \
  -H "Content-Type: application/json" \
  -d '{"orderNumber":"P-2026-0042","customerEmail":"ana@example.com"}'
```

- Only POST is accepted. The token is part of the address (`whk_` followed by random characters), and the secret (`whsec_…`) goes in the header `X-Webhook-Secret` or as `Authorization: Bearer <secret>`. If both are present, `X-Webhook-Secret` is used. The secret is required on every call. It is a shared secret, compared in constant time; it is not a signature of the body, so a captured request can be replayed.
- The body is parsed as JSON whatever the `Content-Type` says. Send a JSON object (a list or a text also work).
- The body can be at most 256 KiB.
- The address of the workflow in your Filmo app is the host of that app followed by `/api/webhooks/workflows/` and the token.

## Responses

| Status | Body | When |
| --- | --- | --- |
| 202 | `{ "runId": "…" }` | A run was queued. |
| 200 | `{ "runId": "…", "duplicate": true }` | You repeated an `Idempotency-Key` already seen on this workflow: no new run, the id of the existing one. |
| 400 | `{ "error": "invalid_json" }` | The body is empty or is not JSON. |
| 400 | `{ "error": "idempotency_key_too_long" }` | The key is longer than 255 characters. It is refused, not truncated. |
| 401 | `{ "error": "unauthorized" }` | The secret is missing or wrong. |
| 404 | `{ "error": "not_found" }` | Unknown token, the trigger is not a webhook, the workflow is not live, or it is paused. They are all the same answer on purpose. |
| 409 | `{ "error": "no_version" }` | The workflow has never been published. |
| 409 | `{ "error": "conflict" }` | A rare race with a duplicate whose run has disappeared. |
| 413 | `{ "error": "body_too_large" }` | Over 256 KiB. |
| 429 | A message and `retryAfterMs`, with a `Retry-After` header | More than about 60 requests in a minute to this address. The count is kept per server instance, so it is approximate. |

Any other method answers 405.

## The body is checked after the 202

> **A 202 does not mean the body was valid:** Filmo checks the body against the type you declared when the run starts, not when the request arrives. If the body does not match, you have already received 202 and the run fails with a message such as "the webhook body does not match the declared type: …". The sender never finds out. Open the run in the «Runs» to see why.

The rules: extra fields are ignored; every field you declared is required (declare a field as Unknown to accept anything); a number must be a JSON number, not a text; a date must be ISO 8601; an enumeration must be one of its options. The variable `trigger_body` holds the body as you sent it, including the fields you did not declare.

## Don't start the same run twice

- Send an `Idempotency-Key` that identifies the thing that happened (an order number and what happened to it), not the attempt. A redelivery with the same key returns 200 and the original run. The key is unique per workflow, not across workflows.
- Without the header, nothing is deduplicated: two identical deliveries start two runs, because two identical bodies can be two real events. Filmo never uses the body to decide.
- The protection lasts as long as the run exists. Finished runs are deleted 90 days after they end.

## The credentials

- The token and the secret are created with the workflow, for every workflow, even if it never uses a webhook. They do not belong to a version: they do not change when you save, publish, change the trigger or restore anything.
- There is no way to rotate or regenerate them. If a secret leaks, the remedy is to build a new workflow, which gets new credentials.
- They are shown to everyone who can open the workflow in the editor. For a «Public» workflow that means any member of your company. If that is not what you want, switch the workflow to «Private»: only its creator and administrators can then open it.

## What runs, and as whom

The run starts with the published version, executes with the permissions of the workflow's creator, and sees the body in `trigger_body`. Delivery is at least once for the effects of its steps: see [Errors and retries](https://docs.filmo.io/workflows/errors-and-retries#at-least-once).

> **Tip:** To see it end to end, the guide [Start a workflow from your own system](https://docs.filmo.io/workflows/guides/start-from-your-system) sets up a webhook, a type and a test delivery.

---

Canonical URL: https://docs.filmo.io/workflows/incoming-webhooks  
Last updated: 2026-10-09