Implement a webhook receiver
This article describes the webhook delivery format, HTTP headers, signature verification, event types, and error codes for developers building webhook receiver integrations.
This article describes the key components you need as a developer to implement a webhook receiver that accepts Cerby event deliveries. The following sections are covered:
For an overview of how webhooks work and how to create a webhook endpoint in Cerby, refer to Explore webhook notifications and Create a webhook.
Envelope format
Every webhook request body is a JSON object with a fixed top-level structure. The error field is present only on failure events. It is absent (not null) on all other event types.
The following is an example of a success event delivery:
{
"envelope_version": "1.0",
"event_id": "01977f2e-9c1a-7d3b-8f00-2b9a4c5d6e7f",
"event_type": "account.credentials.rotated",
"occurred_at": "2026-06-09T20:12:01.342Z",
"workspace_id": "9f8e7d6c-5b4a-4f3e-9d2c-1b0a99887766",
"data": { "account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "application": "Salesforce", "credentials_identifier": "j.doe@acme.com" },
"trace_id": null,
"correlation_id": null
}The following is an example of a failure event delivery (automation.failed):
The following table describes each field in the JSON object:
envelope_version
string
Always "1.0" in this release. New fields may be added without a version bump.
event_id
string (UUIDv7)
Unique per event. Stable across all retry attempts for the same event. Use as the per-event idempotency key.
occurred_at
string (RFC 3339, UTC, ms)
When Cerby recorded the event. Not the source-system time.
workspace_id
string (UUID)
The Cerby workspace where the event occurred.
data
object
Event-specific payload. Fields vary by event type.
error
object
Present only on failure events. Contains { "code": string, "message": string | null, "user_action": string | null }. Absent (not present) on all other event types.
trace_id
null
Reserved. Always null in this release.
correlation_id
null
Reserved. Always null in this release.
Forward compatibility
Apply the following forward compatibility rules when processing deliveries:
Ignore unknown fields. New top-level keys may appear without a version bump.
Tolerate null on
trace_id,correlation_id,error.message, anderror.user_action.Do not assume field ordering.
Treat unknown
error.codeanderror.user_actionvalues as opaque strings. Do not reject events because of an unrecognized value.
HTTP headers
Every webhook request includes the following headers:
X-Cerby-Signature
<algo>=<base64url(sig)> where <algo> is ed25519 or hmac-sha256. Repeated during a key rotation (new key first, old key second).
X-Cerby-Signature-Key-Id
UUID of the signing key used. One header per signature row, in the same order as X-Cerby-Signature.
X-Cerby-Signature-Timestamp
RFC 3339 millisecond-precision UTC timestamp. The timestamp is part of the signed message. Reject requests where it is more than five minutes from the current time.
X-Cerby-Delivery
UUID constant across all retry attempts for the same event and webhook. Use as the per-delivery idempotency key.
X-Cerby-Attempt
Attempt number starting at 1, incremented on each retry, up to 6.
Idempotency-Key
Same value as X-Cerby-Delivery. Provided as a convenience alias.
Signature verification
Verify the signature on every incoming request before processing the event. A request that fails verification must be rejected with a 4xx response.
How the signature is constructed
Cerby signs the following byte sequence:
Where <timestamp_ms> is the X-Cerby-Signature-Timestamp header converted to an integer number of milliseconds since the Unix epoch, . is a literal period, and <raw_body_bytes> is the unmodified request body as received. There is no JSON canonicalization.
Verification algorithm
To verify an incoming request, complete the following steps:
Parse
X-Cerby-Signature-Timestampto milliseconds. Reject the request if|now_ms - ts_ms|exceeds 300,000 (five minutes).Build the signed byte sequence:
b"{ts_ms}." + raw_body.For each
(X-Cerby-Signature, X-Cerby-Signature-Key-Id)pair:Skip rows whose algorithm prefix you do not handle.
Look up the public key or shared secret for that key ID.
Decode the signature value: strip the
ed25519=orhmac-sha256=prefix, then base64url-decode. The value is unpadded. Re-pad to the next multiple of four before decoding (==for Ed25519,=for HMAC-SHA256). Do not hard-code==across both algorithms.Verify. Accept on the first row that validates. Refer to Key rotation for how this applies during a key change.
If no row validates, reject the request.
Reference implementations
The following implementations are kept in sync with the CI test suite. Adapt the input plumbing to your framework before use.
IMPORTANT: The following mistakes each cause every valid delivery to fail silently, producing a blanket reject that looks identical to a real signature failure.
Use the raw request body, not a re-serialized object. Parsing the JSON then calling
JSON.stringify(req.body)(or equivalent) produces different bytes than Cerby signed. Capture the raw body before any parsing, for example usingexpress.raw()in Node.js.Split repeated
X-Cerby-Signatureheaders. During a key rotation, Cerby sends two signature rows. Many frameworks collapse repeated headers into a comma-separated string. Split the value and build separatesigs[]andkids[]arrays before iterating. A verifier that only checks the first row rejects all rotated deliveries.Fail closed on parse errors. Treat a missing header or malformed body as a verification reject and respond 4xx, not 5xx. Cerby treats 5xx as retryable and will redeliver indefinitely.
Python (Ed25519)
The following is the CI-tested reference implementation in Python.
Node.js (Ed25519, Node 16+)
The following is the CI-tested reference implementation for Node.js 16 and later.
Key rotation
Retrieve your public key from GET /v1/event-webhooks/{webhook_id}/keys. During a rotation, this endpoint returns both the primary and secondary keys. Key your lookup by X-Cerby-Signature-Key-Id so each row resolves to the correct key.
When Cerby rotates a key, it dual-signs every request for 24 hours with both the old key (secondary) and the new key (primary), new key first. Accept the request if either row validates. After 24 hours, the old key expires and only the new key is used.
Cache-miss as the rotation signal. If an incoming request carries a X-Cerby-Signature-Key-Id you do not recognize, treat that as a signal that a rotation has started: re-fetch both public keys from the /keys endpoint, update your local lookup, then verify. Do not reject on an unknown key ID before refreshing.
Retries of a delivery that started before a rotation and complete after it arrive signed under the new key. Because you verify by key ID and accept on any valid row, this is transparent.
Event types
This section lists the event types available for subscription, grouped by domain. Each subsection describes the data fields specific to that event type, as defined in the Envelope format.
Account events
The following table lists account event types available for subscription:
Event type
data fields
Notes
account.created
account_id, domain, vault_id, requestor_id
account.deleted
account_id, requestor_id
Not delivered in this release.
account.disabled
account_id, requestor_id
account.enabled
account_id, requestor_id
account.credentials.updated
account_id, application, credentials_identifier
credentials_identifier is the username or email (PII).
account.credentials.rotated
account_id, application, credentials_identifier
Rotation-sourced credential change.
account.credentials.rotation_failed
account_id, application, automation_job_id, automation_link
Carries an error object. Provisional.
account.access.shared
account_id, account_name, new_teams_ids, role
account_name is PII. May co-fire with account.access.shared_with_user.
account.access.shared_with_user
account_id, account_name, new_users_ids, role
May co-fire with account.access.shared.
account.login.failed
account_id, application, automation_job_id, automation_link
Carries an error object. Provisional.
account.mfa.enabled
account_id, application, automation_job_id
Provisional. Correlates with account.mfa.setup_failed by automation_job_id.
account.mfa.disabled
account_id, provider
account.mfa.setup_failed
account_id, application, automation_job_id, automation_link
Carries an error object. Provisional.
Automation lifecycle events
All automation events include the base data fields: automation_job_id, application, account_id, automation_link. The following table lists automation lifecycle event types:
Event type
Extra data fields
Notes
automation.users.provisioned
child_count, child_failure_count
Rollup: one event per execution group.
automation.users.deprovisioned
child_count, child_failure_count
Rollup.
automation.users.role_updated
child_count, child_failure_count
Rollup.
automation.password.rotated
—
automation.mfa.enabled
—
automation.universal_logout.completed
—
Provisional.
automation.succeeded
action
Always-fire catch-all. Co-fires alongside the matching specific automation.* success event for the same job.
NOTE: automation_link is the public API URL for the source automation job (GET https://<workspace>.cerby.com/api/v1/jobs/<automation_job_id>). Fetching it requires an X-API-Key with the Read automated jobs (read:automations) scope. Two preconditions apply: the workspace must have public API access enabled, and the key's user must have visibility of the job's target account. A key user without account visibility returns 401; a key missing the scope returns 403. It is not a browser link. To inspect the job interactively, use the Automation Dashboard in Cerby.
NOTE: child_count and child_failure_count are present but null in this release for rollup events.
NOTE: action is an opaque pass-through of the internal workflow code that identifies the automation type (for example, password_rotation, provide_access). Treat any value as an opaque string. Do not condition logic on specific values.
Provisional event types
Event types marked Provisional are defined in the catalog but do not yet have a confirmed delivery emitter in all cases. They may not arrive in production. The catalog will be finalized before the GA release.
Reserved event types
The following event types are reserved for a future release. Subscribing to them currently returns HTTP 400:
account.vault.assignedaccount.trusted_session.establishedautomation.users.attributes_updatedautomation.users.synced
Automation failure codes
The automation.failed event type covers all automation failures. The specific failure is identified by error.code, not by the event type. Individual error codes are not separately subscribable.
The error object has three fields:
code
string
Stable machine identifier. Treat unrecognized values as opaque generic failures.
message
string or null
Human-readable description. May be null for some codes. Do not rely on its exact wording.
user_action
string or null
Coarse remediation category (see table below). Null or unknown values should be treated as "review in Cerby".
The following error codes are available in this release:
error.code
Tier
error.user_action
data.remediation
NotEnoughSeats
0
add_licenses
{ seats_required, seats_available, license_type }
SubscriptionDeactivated
0
reactivate_subscription
null
UnderpermissionedAccount
0
grant_permissions
null
InvalidBusinessId
0
fix_business_id
null
BadCredentials
1
review_credentials
null
EmptyPasswordError
1
review_credentials
null
AccountVaultError
1
contact_support
null
EmailNotProvidedError
2
update_contact_info
null
InvalidEmail
2
update_contact_info
null
PhoneNotProvidedError
2
update_contact_info
null
PhoneNotManagedError
2
configure_mfa
null
GeneralMFANotManagedError
2
configure_mfa
null
MFANotManagedError
2
configure_mfa
null
MFAAlreadyEnable
2
configure_mfa
null
OtherMFAMethodEnabledError
2
configure_mfa
null
VerificationMethodsNotManagedError
2
configure_mfa
null
SecureDeviceNotFoundInStorage
3
reauthenticate
null
NOTE: MFAAlreadyEnable is the exact wire value. The missing "d" is intentional. Do not correct it in code or documentation.
The following table describes each user_action value:
user_action
Meaning
review_credentials
Update the username or password stored in Cerby
update_contact_info
Add or correct an email or phone number in Cerby
configure_mfa
Set up or transfer MFA management to Cerby
reauthenticate
Re-establish the Cerby browser extension trusted session
contact_support
Contact Cerby Support to resolve the issue
add_licenses
Purchase additional seats in the target application
reactivate_subscription
Reactivate the target application subscription
grant_permissions
Grant the Cerby-managed account the required permissions
fix_business_id
Correct the business or tenant ID configured in Cerby
Tier 0 codes (add_licenses, reactivate_subscription, grant_permissions, fix_business_id) represent failures that must be remediated outside Cerby before retrying the automation. Only NotEnoughSeats populates data.remediation with machine-readable detail in this release. All other codes deliver data.remediation: null.
Delivery semantics
This section describes the guarantees your receiver can rely on when processing Cerby webhook deliveries: how to handle duplicate deliveries, how Cerby retries failed requests, and how your endpoint's response code determines what happens next. Understanding these semantics will help you build a receiver that fails safely instead of dropping events or exhausting the retry budget.
At-least-once and deduplication
Cerby guarantees at-least-once delivery. Your receiver may receive the same event more than once: on retries, or in rare cases as replays. Deduplicate as follows:
Per delivery: Use
X-Cerby-Delivery(constant across retries) combined withX-Cerby-Attempt(rising). A retry is identified only by a repeatedX-Cerby-Deliverywith a higherX-Cerby-Attempt.Per event: Use
event_idto collapse the same logical event across different webhooks or delivery channels.
Do not deduplicate on payload content, URL, or signature equality.
Retry policy
The following table describes the retry policy parameters:
Max attempts
6 (1 initial + 5 retries)
Initial interval
60 seconds
Backoff
×4 per attempt (approximately 1 min, 4 min, 16 min, 64 min, 4.3 h)
Max interval
6 hours
Hard deadline
24 hours total
Response handling
The following table describes how Cerby handles each response code from your endpoint:
2xx
Delivery complete.
408, 429, or 503 with Retry-After
Retried. Cerby honors Retry-After, clamped to one hour.
Any other 4xx
Fail-fast. The delivery is dead-lettered immediately, not retried.
5xx, timeout, or TCP error
Retried with standard backoff.
A 401 or 404 response fails fast and will not heal on retry. Fix the endpoint configuration and use Cerby's redelivery tooling to re-send if needed.
Respond 2xx quickly and process the event asynchronously. Slow synchronous processing risks burning your retry budget.
Sending to a Slack Incoming Webhook
When your endpoint URL is a Slack Incoming Webhook (hooks.slack.com), Cerby delivers a Slack Block Kit message instead of the standard JSON envelope. This format is required for successful Slack delivery.
The Block Kit message is a lossy subset of the full event payload. It includes the following fields:
Event type
Workspace ID
Application (when present in
data)Account ID (when present in
data)Automation job ID (when present in
data, labeled "Automation Job"), rendered as plain textoccurred_attimestampFor failure events:
error.code,error.message, anderror.user_action(labeled as "Remediation")
Fields present in the standard envelope that are not included in the Block Kit message include data.action, data.automation_link, and all other data fields not listed above. HTTPS receivers receive the full payload including these fields.
Cerby colors the attachment sidebar based on severity: #2EB67D (green) for success events and #D50200 (red) for failure events.
The following is an example Block Kit message for a success event:
Current release limitations
The following limitations apply to webhooks in this release:
account.deletednot delivered: The event type is available for subscription but Cerby does not deliver it in this release.Canada region not supported: Webhooks are not available for Canada-resident workspaces (
ca-central-1) in this release.Most events are standalone: Events are generally not linked to related events.
trace_idandcorrelation_idare reserved and always null in this release. The one exception: anautomation.failedevent and its eventualautomation.succeededevent carry the sameautomation_job_id, so you can pair a failure with its later success using that field.Provisional event types: Several event types are defined in the catalog but do not yet have confirmed delivery in all cases. These are marked as provisional in the Event types section above.
Related articles
Feature guides:
Troubleshooting:
Last updated
Was this helpful?

