# Rotate a receiving domain's signing secret

`POST /v1/inbound/domains/{domain}/webhook/rotate-secret`

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

**Irreversible, and classified destructive — a test-environment
credential is refused.**

There is NO grace window, unlike notification webhooks' 24 hours: your
intake function reads ONE secret out of the published routing, so every
signature computed with the previous secret stops verifying as soon as
that routing is republished, which is about a minute. Deploy the new
secret to your endpoint first, or accept a window in which real mail is
rejected.

The new secret is returned once and cannot be shown again. There is no
request body.

422 `webhook_not_set` when the domain has no webhook to rotate.

## 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.

## Example request

```bash
curl -X POST 'https://api.sendops.dev/v1/inbound/domains/string/webhook/rotate-secret' \
  -H "Authorization: Bearer $SENDOPS_API_KEY"
```

## Responses

### 200 — The new signing secret, shown once

Content type: `application/json`

```json
{
  "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"
  ]
}
```
