curl --request POST \
--url https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"overlap_seconds": 3600
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({overlap_seconds: 3600})
};
fetch('https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret"
payload = { "overlap_seconds": 3600 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret"
payload := strings.NewReader("{\n \"overlap_seconds\": 3600\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "0199a2f0-1b00-7a11-8c22-3d4e5f6a7b8c",
"url": "https://merchant.example/hooks/citadel",
"description": "Production backend",
"event_types": [
"operation.confirmed",
"operation.failed",
"blockchain_event.confirmed"
],
"state": "active",
"disabled_at": null,
"consecutive_failures": 0,
"previous_secret_expires_at": "2026-10-09T09:00:00Z",
"created_at": "2026-10-01T09:00:00Z",
"updated_at": "2026-10-09T08:00:00Z",
"secret": "whsec_Zp41…"
}Rotate the signing secret
Generates a new signing secret and shows it in this answer only. The secret it replaces
keeps signing next to it for overlap_seconds: until previous_secret_expires_at, every
delivery carries two v1 signatures in X-Citadel-Signature, the new secret’s first.
Deploy the new secret to your verifier within that time and no delivery is rejected on the
way.
Only two secrets sign at once. Rotating again during an overlap retires the older one at
once; the secret being replaced becomes the previous one. overlap_seconds: 0 retires the
old secret now — deliveries already on their way may then fail your check once and are
retried with the new one.
Not idempotent: every call makes a new secret. A retry after a lost answer replaces the
secret of that answer, so the one your verifier holds stops signing at once: deploy the
secret from the last answer you got. overlap_seconds goes in the body; in the query
string it answers 422.
Needs the webhooks.write scope. A key limited to vaults is refused with
403 vault_limited.
curl --request POST \
--url https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"overlap_seconds": 3600
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({overlap_seconds: 3600})
};
fetch('https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret"
payload = { "overlap_seconds": 3600 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.citadel.example/api/v1/webhook_endpoints/{id}/rotate_secret"
payload := strings.NewReader("{\n \"overlap_seconds\": 3600\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "0199a2f0-1b00-7a11-8c22-3d4e5f6a7b8c",
"url": "https://merchant.example/hooks/citadel",
"description": "Production backend",
"event_types": [
"operation.confirmed",
"operation.failed",
"blockchain_event.confirmed"
],
"state": "active",
"disabled_at": null,
"consecutive_failures": 0,
"previous_secret_expires_at": "2026-10-09T09:00:00Z",
"created_at": "2026-10-01T09:00:00Z",
"updated_at": "2026-10-09T08:00:00Z",
"secret": "whsec_Zp41…"
}Authorizations
An API key minted in the console (ctd_…). Authenticates; never authorizes funds. A key
carries scopes (read, wallets.write, transactions.write, events.write,
webhooks.write, policy.write) and may be limited to vaults, to IP addresses, and in
lifetime — see Authentication.
Path Parameters
The endpoint id — id in its row, endpoint.id in a delivery.
Body
How long the old secret keeps signing, in seconds. 0 retires it now; at most 7 days.
0 <= x <= 604800Response
Rotated. The answer carries the new secret, once.
The endpoint and its signing secret, shown in this answer only.
Where Citadel posts. An https:// URL with a host.
What the endpoint is for — your words.
The events it hears. [] hears every event.
operation.confirmed, operation.failed, operation.expired, operation.stuck, operation.awaiting_approval, operation.approved, blockchain_event.confirmed, run.completed, run.failed, wallet.active, wallet.failed active receives its events. paused: you paused it. disabled: Citadel turned it off
after 5 deliveries in a row exhausted their attempts. Neither of the last two gets a
delivery, not even a later one: PATCH with state: active resumes it.
active, paused, disabled When it was paused or disabled; null while active.
Deliveries in a row that exhausted their attempts. At 5 the endpoint is disabled; a delivered one, or a resume, sets it back to 0.
While a rotated secret still signs next to the new one, until when; else null.
The signing secret (whsec_…). Citadel shows it here only — store it now.