Each receiving domain has one **webhook**: the endpoint its mail is delivered to, plus the rules for which recipients and which verdicts get delivered at all. It is set on the domain, under **Infrastructure → Inbound → Settings**.

Until a verified domain has a webhook, mail to it lands in your bucket and is recorded as parked with the reason **unknown domain**. Nothing is lost — it can be redelivered once a webhook exists — but nothing is delivered either.

## The URL

- **HTTPS only.**
- **The host must resolve to a public address.** SendOps resolves it and checks every address it returns; a host that resolves to a private, loopback or link-local address is refused, and so is a host that does not resolve at all. The refusal never names the address.
- Both checks run when you save **and again every time the routing is published**, because what DNS answers changes. A host that later stops resolving, or starts resolving privately, puts the webhook into the **invalid** state described below.

The request your endpoint receives comes from the intake function **in your own AWS account**, not from SendOps, so there is no IP range to allow-list. Verify the signature instead — see [the webhook contract](https://developers.sendops.dev/api-reference/inbound-webhook), which is the page to hand to whoever writes the endpoint.

## The allow list

**Allowed recipients** is a list of **local-part patterns** — the part before the `@`. `support` matches exactly that address; `ticket-*` matches any address starting `ticket-`; `*` on its own matches everything. Matching is case-insensitive, and an entry may not contain an `@`. Up to 100 patterns.


  An **empty** allow list delivers **every** address at the domain. That is the opposite of how an empty list usually reads, and it is the most consequential setting on the page. A recipient that matches nothing in a non-empty list is recorded as **no route** and is never delivered.


Saving the webhook is a **full replacement**: what you save is the whole list. An address you later mint through the [Public API](https://developers.sendops.dev/api-reference/inbound) is added to a non-empty list for you.

## Spam and viruses

SES scans every message and reports a verdict for spam, virus, SPF, DKIM and DMARC. What a **failing** spam or virus verdict does is set per domain, under **When the spam scan fails** and **When the virus scan fails**:

| Posture | Effect |
|---|---|
| **Deliver, with the verdict in the payload** (`tag`) | Your endpoint decides. |
| **Deliver nothing** (`drop`) | The message is recorded as **dropped** in the log. |

Defaults: spam is delivered with the verdict, a virus is dropped. SPF, DKIM and DMARC results are always stated in the payload and never drop a message on their own.

## The signing secret

Every request to your endpoint is signed with a per-domain secret. The secret is minted **the first time you save the webhook** and shown **once**, in a dialog you cannot reopen: SendOps stores it encrypted and cannot display it again. Copy it into your endpoint's configuration before closing the dialog.

Changing the URL later keeps the secret — correcting a typo is not a request to re-key.

**Rotate secret** mints a new one and shows it once. There is **no grace window**: the intake function reads a single secret from the published routing, so signatures made with the old secret stop verifying as soon as the routing is republished, about a minute later. Deploy the new secret to your endpoint first, or accept a window in which real mail is rejected.

## Testing the endpoint

**Test webhook** POSTs a synthetic, signed payload — obviously fake content, `test: true`, an all-pass set of verdicts — to the saved URL and reports what your endpoint did: the HTTP status and the response time, or a plain description of a transport failure (no response within ten seconds; the connection failed; the address is one SendOps will not dial).

A `4xx` or `5xx` from your endpoint is a **result**, not an error: your endpoint answered, which is what the test asks. You can run ten tests an hour per domain.


  Check, in this order: the domain's state (**Waiting for DNS** means the MX or the identity is not published), the webhook's publication state below, the allow list (a recipient matching nothing is **no route**), and then the [message log](/inbound/message-log), where a parked message says why.


## Publication states

What you save has to reach the routing configuration your intake function reads, an object SendOps writes into your bucket. The webhook's state tracks that:

| State | Meaning |
|---|---|
| **Pending** | Saved; the routing has not been re-published yet. Normally closes within seconds, and always within about a minute. Until then, mail for the domain has no route. |
| **Published** | Live. |
| **Invalid** | The URL failed re-validation at publish time — most often a host that stopped resolving, or now resolves to a private address. The domain was left out of the routing entirely, and the error says why. Fix the URL and save. |

This state is separate from the domain's DNS state. A domain can be **Verified** with a **Pending** webhook, or **Waiting for DNS** with a **Published** one; a domain is included in the routing only when it is both verified and has a webhook.

## Removing the webhook

**Remove webhook** removes the URL, the allow list, the postures and the secret, and republishes without the domain. Mail keeps arriving and keeps landing in your bucket; it stops being delivered and is recorded as parked, reason **unknown domain**. Saving a webhook again restores delivery — with a **new** secret, since the old one was discarded.

## Related pages

- [Message Log](/inbound/message-log) — what each outcome means, and Redeliver
- [Webhook payload and signature](https://developers.sendops.dev/api-reference/inbound-webhook) — the contract your endpoint implements
- [Inbound as Code](/inbound/inbound-as-code) — the same settings, declared in your repository