# Set where a receiving domain's mail is delivered

`PUT /v1/inbound/domains/{domain}/webhook`

- Authentication: required (Bearer token)
- Required scope: `api.inbound.manage`

**Full replacement, not a patch.** An omitted `allow_patterns` means
"deliver every recipient at this domain", not "leave the list as it
was". That is the field on this surface most likely to be got wrong,
because the two readings differ by the whole allow-list.

`hmac_secret` is present in the response ONLY when this call minted it —
the first webhook save on a domain. It survives a URL change, because
correcting a typo in an endpoint is not a request to re-key it, and
re-keying silently would break signature verification on the endpoint
you just fixed. Store it when you receive it; if you lose it, rotate.

The URL must be `https` and its host must resolve to a public address.
Both are checked here AND again at publish time, because what DNS
answers changes. A refusal is 422 with the detail on `webhook_url`, and
no refusal ever names an address.

Saving re-publishes the routing into your landing bucket in the
background, so the change reaches your intake function within about a
minute.

The payload your endpoint then receives, and how to verify its
signature, is documented at
`https://developers.sendops.dev/api-reference/inbound-webhook`.

## Path parameters

- `domain` (string, required) — The receiving domain, by NAME (`in.acme.com`). A UUID is also accepted. Matched case-insensitively, and scoped to the calling org — a domain belonging to another org is a 404, never a 403.

## Request body

Content type: `application/json`

```json
{
  "webhook_url": "https://example.com",
  "allow_patterns": [
    "string"
  ],
  "spam_posture": "tag",
  "virus_posture": "tag"
}
```

## Example request

```bash
curl -X PUT 'https://api.sendops.dev/v1/inbound/domains/string/webhook' \
  -H "Authorization: Bearer $SENDOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com","allow_patterns":["string"],"spam_posture":"tag","virus_posture":"tag"}'
```

## Responses

### 200 — The receiving domain, plus the HMAC secret when this call minted it

Content type: `application/json`

```json
{
  "domain": "string",
  "id": "00000000-0000-0000-0000-000000000000",
  "status": "pending_dns",
  "last_error": "string",
  "region": "string",
  "cross_region": true,
  "mx_verified_at": "2026-05-17T20:00:00Z",
  "identity_verified_at": "2026-05-17T20:00:00Z",
  "records": [
    {
      "type": "MX",
      "name": "string",
      "value": "string",
      "verified": true
    }
  ],
  "webhook": {
    "url": "https://example.com",
    "allow_patterns": [
      "string"
    ],
    "spam_posture": "tag",
    "virus_posture": "tag",
    "status": "unset",
    "error": "string",
    "published_at": "2026-05-17T20:00:00Z"
  },
  "created_at": "2026-05-17T20:00:00Z",
  "hmac_secret": "string"
}
```

### 401 — Missing, malformed, or unknown API key

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 403 — Either the credential lacks the required scope (`code: invalid_scope`), or it is bound to the `test` environment and this operation is irreversible (`code: test_environment_forbidden`). Branch on `code`: the first is fixed by granting the scope, the second only by using a live credential. See the "Live and test credentials" section of the API description.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 404 — Resource not found

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 422 — A query parameter, path value or body field failed validation.

The body is a `validation_failed` Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries `validation_code`,
`field`, `line`/`column`, `missing`, `candidates` and `next_step`, so a
client can correct the call without a second round of guessing. See
`ValidationProblem`.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ],
  "validation_code": "invalid_syntax",
  "field": "string",
  "line": 0,
  "column": 0,
  "missing": [
    {
      "kind": "segment",
      "key": "string"
    }
  ],
  "candidates": {},
  "next_step": "string"
}
```

### 429 — Per-org rate limit exceeded

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 500 — Unexpected server-side failure. The `code` is `internal_error`. The
`request_id` field can be quoted to SendOps support to investigate.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```
