Webhook Endpoints
Webhook destinations are configured on the organization, not on the flow. An organization owns a registry of endpoints; each endpoint has its own URL, its own event subscriptions, its own flow scope, its own signing secret, and its own payload version.
A flow used to carry a single webhookUrl, and every event that flow produced went there. That field no longer exists: the flow API neither accepts nor returns webhookUrl. Destinations live at /organizations/{organizationId}/webhooks, and one event can now fan out to several endpoints.
Existing configurations were converted automatically and keep delivering byte-for-byte the payload they received before. See What was migrated for you.
Anatomy of an endpoint
| Field | Meaning |
|---|---|
name | Label used in the dashboard and in delivery diagnostics. Up to 120 characters, unique within the organization. |
url | HTTPS destination. Up to 2048 characters, re-validated against the public-address rules before every delivery. |
enabled | Disabling an endpoint stops deliveries immediately, including work already queued. |
eventTypes | One or more canonical event types. At least one is required. See Event subscriptions. |
flowFilter | OWNED_ALL or SELECTED plus an explicit flow list. See Flow scope. |
secretMode | ORGANIZATION (the shared secret) or DEDICATED (a secret only this endpoint uses). See Signing secret. |
payloadVersion | V2 or LEGACY_V1. See Payload Versions. |
secretHint | Last characters of the secret in force, so you can tell which key an endpoint signs with. The secret itself is never read back. |
managedByFlowId | Set when the endpoint was created by the one-shot migration from a flow's old webhookUrl. Provenance only — the endpoint is fully editable. |
Endpoints are per-environment: the sandbox registry and the production registry are separate. Every route accepts the usual ?sandbox=true convention.
Event subscriptions
An endpoint receives only the event types it subscribes to. The canonical vocabulary is resource-specific (verification.dv.completed, flow.completed, transaction.alert.created, …) and is separate from the top-level event field inside the payload, which is coarser and cannot distinguish a DV result from a KYB result.
The full catalog is listed in Statuses and codes, and the deployment's live copy is served by:
GET /organizations/{organizationId}/webhooks/events
Only catalog values are accepted. Wildcards are not: the API returns advisory groups (VERIFICATION, FLOW, TRANSACTION, KYC_PASSPORT) for building a selection UI, but a subscription always stores exact values, so adding an event to the catalog later can never silently widen a subscription you already made.
Flow scope
| Scope | Receives |
|---|---|
OWNED_ALL | Every flow owned by your organization, including flows created later. |
SELECTED | Only the flows you list. Up to 100 flows per endpoint. |
OWNED_ALL deliberately excludes flows that belong to a partner organization: accepting a new KYC Passport partnership never widens an existing endpoint's data feed.
Events that have no source flow — today transaction.alert.created — ignore the flow filter rather than failing it, so a SELECTED endpoint that subscribed to Transaction Monitoring alerts still receives them.
Signing secret
| Mode | Behaviour |
|---|---|
ORGANIZATION | Signs with the organization's shared secret. The default, and what every migrated endpoint uses. Rotating the shared secret re-keys every endpoint in this mode at once. |
DEDICATED | Signs with a secret owned and rotated by this endpoint alone. |
Rules that follow from that split:
- A plaintext secret is returned exactly once — by the create, secret-mode-switch, and rotate operations — and can never be read back. Store it when you receive it.
ORGANIZATIONmode requires the organization to already have a shared secret.POST /{endpointId}/rotate-secretis valid only forDEDICATEDendpoints.- Rotating the shared secret takes effect immediately for new attempts. The outgoing value stays recorded as acceptable for a one-hour grace window —
secretExpiresAton the organization-secret response — so keep accepting it on your side until then. - Changing an endpoint's URL, filters, or payload version never rotates a secret.
Verification itself is unchanged and is documented in Webhook Signature.
Limits
| Limit | Value |
|---|---|
| Active endpoints per organization and environment | 30 |
| Selected flows per endpoint | 100 |
| Endpoint name | 120 characters |
| Endpoint URL | 2048 characters |
Duplicate URLs are allowed: two endpoints may point at the same destination when their filters, secrets, or payload versions differ.
Who can manage endpoints
The Webhooks surface is restricted to Admin (ORG_ADMIN) and Developer (ORG_DEVELOPER). See the role matrix.
API
All routes are under /organizations/{organizationId}/webhooks and are authenticated with a dashboard session.
| Method | Path | Purpose |
|---|---|---|
GET | / | List endpoints |
POST | / | Create an endpoint |
GET | /{endpointId} | Read one endpoint |
PATCH | /{endpointId} | Update name, URL, enabled, events, filter, version |
DELETE | /{endpointId} | Delete an endpoint |
POST | /{endpointId}/rotate-secret | Rotate a dedicated secret |
POST | /{endpointId}/secret-mode | Switch between ORGANIZATION and DEDICATED |
GET | /organization-secret | Shared-secret status (never the secret itself) |
POST | /organization-secret/rotate | Rotate the shared secret |
GET | /events | The canonical event catalog |
Create an endpoint
POST /organizations/{organizationId}/webhooks?sandbox=true
Content-Type: application/json
{
"name": "Onboarding results",
"url": "https://api.example.com/webhooks/zyphe",
"enabled": true,
"eventTypes": ["verification.dv.completed", "verification.dv.failed", "verification.dv.review", "flow.completed"],
"flowFilter": {
"scope": "SELECTED",
"flowIds": ["89b37f2c-544e-4c49-bd52-e03208f94cc7"]
},
"secretMode": "ORGANIZATION",
"payloadVersion": "V2"
}
enabled defaults to true, secretMode to ORGANIZATION, flowFilter to OWNED_ALL, and payloadVersion to V2.
The response is the stored endpoint, plus secret when the operation minted a dedicated one:
{
"id": "1f6b0d3c-2a1e-4c58-9d47-0b0f3f6b1e21",
"organizationId": "6b0f6a2e-1f5c-4a70-9d6b-2b1a0e4c9d33",
"name": "Onboarding results",
"url": "https://api.example.com/webhooks/zyphe",
"enabled": true,
"eventTypes": ["verification.dv.completed", "verification.dv.failed", "verification.dv.review", "flow.completed"],
"flowFilter": { "scope": "SELECTED", "flowIds": ["89b37f2c-544e-4c49-bd52-e03208f94cc7"] },
"secretMode": "ORGANIZATION",
"secretHint": "…a91c",
"secretUpdatedAt": "2026-08-20T09:14:02Z",
"payloadVersion": "V2",
"managedByFlowId": null,
"createdAt": "2026-08-26T10:00:00Z",
"updatedAt": "2026-08-26T10:00:00Z"
}
Update an endpoint
PATCH is a partial update: omitted fields are left alone. A supplied eventTypes or flowFilter replaces the previous one wholesale, because a filter is a complete statement of intent.
{ "payloadVersion": "V2", "enabled": true }
secretMode is not patchable here; it has its own route, because changing it mints or retires secret material.
Rotate the shared secret
POST /organizations/{organizationId}/webhooks/organization-secret/rotate
{
"secret": "…the new plaintext, returned once…",
"configured": true,
"secretHint": "…7f3d",
"secretUpdatedAt": "2026-08-26T10:05:00Z",
"secretExpiresAt": "2026-08-26T11:05:00Z",
"affectedEndpointIds": ["1f6b0d3c-2a1e-4c58-9d47-0b0f3f6b1e21"]
}
affectedEndpointIds is the blast radius: every endpoint that now signs with this secret. secretExpiresAt is when the previous secret stops being honoured.
Validation
A write is rejected when:
- the URL is not HTTPS or does not resolve to a public address;
eventTypesis empty or contains a value outside the catalog;flowIdsis supplied withOWNED_ALL, or exceeds 100 entries;- a selected flow is not owned by the organization (partner flows require an accepted, non-revoked KYC Passport invite and the KYC Passport event type);
secretModeisORGANIZATIONbut the organization has no shared secret;- a
LEGACY_V1endpoint subscribes to an event that has no legacy payload — a subscription that could never fire is refused at write time rather than silently dropped at delivery time. See Payload Versions.
Delivery
| Property | Value |
|---|---|
| Method | POST, application/json |
| Success | 2xx for V2 endpoints, strictly 200 for LEGACY_V1 endpoints |
| Attempts | Up to four (one initial plus three retries), exponential backoff from 1s to 8s |
| Request timeout | 30 seconds by default |
| Redirects | Not followed |
Each endpoint is an independent delivery: one endpoint failing or being disabled never affects another endpoint's copy of the same event, and each retries on its own.
A retry uses the URL captured when the delivery was created, not the endpoint's current URL — editing an endpoint never re-points work that was already routed.
Deliveries are deliberately skipped, not failed, when the destination was disabled or deleted after queueing, an SDK session closed or expired, or a KYC Passport consent was revoked between routing and sending.
What was migrated for you
A one-shot migration converted every pre-existing configuration. Nothing about a running integration changed: same URL, same secret, same payload bytes, same environment.
| Old configuration | Becomes |
|---|---|
A flow's webhookUrl | An enabled endpoint named Flow: <slug>, scoped SELECTED to that one flow, subscribed to every flow-bound event, LEGACY_V1, signing with the shared organization secret. |
| Transaction Monitoring URL | An enabled endpoint named Transaction Monitoring (migrated), scoped OWNED_ALL, subscribed to transaction.alert.created, LEGACY_V1, keeping TM's own secret as a DEDICATED one. |
Guarantees the migration kept:
- Secrets were copied, never regenerated. An organization that never configured a secret still receives unsigned deliveries; that state was preserved, not "fixed".
- Endpoints were never merged by URL. Two flows pointing at the same URL remain two independently editable endpoints.
- Endpoints created by the migration carry
managedByFlowIdfor provenance, but behave exactly like ones you create by hand: edit, disable, or delete them freely.
You migrate to the new payload on your own schedule, endpoint by endpoint. See Payload Versions.
The extended DV payload and the legacy data.kyc compatibility object were organization-wide settings, and the migration carried them onto the endpoints it created. They are not settable when you create a new endpoint — a new endpoint always gets the standard payload. If you rely on either and need it on a new endpoint, contact Zyphe support. See Backwards Compatibility.