# Receive webhooks

> Get a webhook URL for your workflow, call it with a service account token, and understand redeliveries, limits, and sender verification.

A webhook trigger gives your workflow a URL. Anything that can send an
authenticated HTTP POST can start a run.

## Outcome

An external system posts to your workflow's URL and gets `202 Accepted` back
with the id of the run it started.

## Before you start

You need an active service account holding the `webhooks` scope, and the
workflow must have a webhook trigger. See [Service
accounts](/help/credentials-connections/service-accounts) and [Choose a
trigger](/help/workflows/choose-a-trigger).

## 1. Get the URL

Save the workflow. Until you do, the trigger inspector says *Save workflow to
generate webhook token* — the token is minted on save.

Afterwards the inspector shows a read-only **Webhook URL** field. The canonical
form is token-only: the token identifies the trigger, the organization, and the
workspace all by itself, so nothing in the URL depends on your organization's
name.

## 2. Call it

```bash
curl -X POST https://app.example.com/api/v1/webhooks/YOUR_TRIGGER_TOKEN \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "A-123", "total": 250}'
```

`$ACCESS_TOKEN` is a short-lived token you exchange your service account's
client ID and secret for, and it must carry the `webhooks` scope. [Service
accounts](/help/credentials-connections/service-accounts) walks through the
exchange.

An accepted delivery returns `202 Accepted` with the id of the run it started:

```json
{"status": "accepted", "workflow_run_id": "0198f2c1-..."}
```

The other answers you can get:

| Status | Means |
|---|---|
| `401` | The credential is missing or wrong. |
| `403` | It authenticated but lacks the `webhooks` scope. |
| `404` | No active trigger matches the token — including a workflow that is not activated. |
| `413` | The body is over 1 MB. |
| `422` | The payload could not be processed. |
| `429` | Over 100 requests per minute for this token. A `Retry-After` header says when to try again. |

Both limits are enforced before any work is done.

## 3. Discover the payload shape

The steps below a trigger can only offer you fields they know about, so the
inspector has an **Output Schema** section with a **Refresh Schema** button.

Press it and Hodoflow starts listening, showing a **Test URL (no data saved)**
and *Waiting for a test POST request...*. Post a representative payload to that
URL and the field names appear as *N fields found*. Nothing is stored and no
run is created.

## Redeliveries and duplicates

Hodoflow recognises a redelivery of something it already accepted and answers
`202` with `{"status": "duplicate"}` instead of starting a second run. A sender
retrying an acknowledgement it never saw therefore stops retrying, without
double-processing.

Identity comes from whichever of these headers your sender provides:
`Idempotency-Key`, `X-GitHub-Delivery`, `X-Shopify-Webhook-Id`, or
`X-Twilio-Idempotency-Token`. With none of them, Hodoflow falls back to hashing
the token and the raw body inside a two-hour window, so a byte-identical
payload is treated as a duplicate within that window and processes normally in
a later one.

## Verifying the sender

A webhook trigger can also point at an inbound connector, chosen under
**Inbound Authentication** in the inspector, to verify signatures or API keys
on top of the service account's token. The picker only appears when your
organization has inbound connectors that carry an authentication method — see
[Inbound connectors](/help/credentials-connections/inbound-connectors).

## Common problems

**Every call returns `404`.** The workflow is not active, or the token belongs
to a version that is no longer live. Only active triggers are served. Activate
the workflow and re-copy the URL.

**Every call returns `401`.** The access token is missing, expired, or was
issued from a deactivated service account. Tokens last 15 minutes; exchange
again rather than caching one.

**Calls return `403`.** The service account authenticated but does not hold the
`webhooks` scope. Add it, then exchange for a new token — a scope change takes
effect on the next exchange.

**The sender says it got `202` but no run appears.** Check the response body.
`{"status": "duplicate"}` means Hodoflow matched it to a delivery it had
already accepted.

**`429` under normal load.** The cap is per trigger token, at 100 requests per
minute. Split high-volume senders across triggers, or batch the payloads.

## Next step

[Configure and test a step](/help/workflows/configure-and-test-steps) to do
something with what arrived. The wire-level reference — request and response
bodies, every status code, header names, and the token exchange — is on the
developer portal at [webhook ingestion](/developer/webhook-ingestion).
