If your workspace is [connected to a repository](/templates/github-integration), receiving domains can be declared there instead of on the Settings page, the way templates and audience definitions are. A domain that comes from the repository is shown with a **git** origin in the dashboard, is read-only there, and links to the file it came from.

## The manifest key

`sendops.json` names the file:



The path is relative to the repository root, must end in `.json`, and may not climb out of the repository. A bad value is reported as a warning on the sync, not as a failed sync.

## The file

A map from receiving domain to its routing:



| Key | Required | Meaning |
|---|---|---|
| `webhook` | yes | The HTTPS endpoint. The same rules as the [dashboard](/inbound/delivery-webhooks#the-url): `https`, resolving to a public address. |
| `allow` | no | Local-part patterns; empty or omitted delivers every recipient. See [the allow list](/inbound/delivery-webhooks#the-allow-list). |
| `spam` | no | `tag` (default) or `drop`. |
| `virus` | no | `tag` or `drop` (default). |

An entry with no `webhook` is invalid and is skipped; the rest of the file still applies.


  No key carries it and none will. The secret is minted the first time a domain's webhook is saved — by the sync as much as by a person — and, because there is nobody to show it to, a secret minted by the sync is **discarded**. A repository-declared domain therefore has no usable secret until you rotate it. Every other control on a git-origin domain is read-only in the dashboard, but **Rotate secret** stays live — it is the one thing the file cannot express — and shows the new secret once. The [Public API](https://developers.sendops.dev/api-reference/inbound) does the same: `POST /v1/inbound/domains/{domain}/webhook/rotate-secret`. A rotated secret survives later pushes, whether or not the declaration changes: an unchanged declaration is not re-saved, and a changed one keeps the secret already in place. Only rotating replaces it.


## What the sync does

On every push, each declared domain is created through the same path a person uses, so the same rules apply: the domain must be, or sit under, one of your domains, and Inbound must be deployed on the AWS account. A domain that fails those is skipped with the reason, and the rest of the run continues. The DNS records still have to be published by you, and verification runs as it does for any domain.

The webhook is saved through the same path as the dashboard's, so URL validation, allow-list normalisation and the postures behave identically. A declaration that has not changed since the last push is left alone rather than re-saved, so pushing unrelated commits does not put every webhook back into **Pending**.

## What the sync will not do

- **It never takes over a domain that was added on the Settings page.** A declaration naming a managed domain is skipped and reported as a collision. There is no promotion from managed to git; delete the managed row first if you want the repository to own it.
- **It never deletes a verified domain.** When a domain disappears from the file, an unverified git-origin domain is removed. A verified one is **kept** and flagged *removed from repository; delete it here* — mail is still landing on it, and deleting a live mailbox is a decision for a person.
- **It never touches a webhook a person could not have saved.** A durable refusal — a URL that will not validate — is written onto the domain as an **Invalid** webhook with the reason, and shows on the Settings page.

## Related pages

- [Receiving Domains](/inbound/receiving-domains) and [Delivery Webhooks](/inbound/delivery-webhooks) — the same settings, by hand
- [Manifest File](/templates/manifest-file) — the rest of `sendops.json`
- [Pull Requests & Compare](/templates/github-integration) — how the repository connection works