Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/test-webhook-integrations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
name: Test webhook integrations
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install handler dependencies
run: |
npm ci --ignore-scripts --prefix cdp-analytics/segment/webhook
npm ci --ignore-scripts --prefix siem-security-analytics/splunk/webhook
npm ci --ignore-scripts --prefix webhook-infrastructure/svix/webhook
npm ci --ignore-scripts --prefix user-enrichment/clearbit/webhook
npm ci --ignore-scripts --prefix crm/pipedrive/webhook
- name: Run isolated regressions
run: node --test scripts/test-webhook-integrations.cjs
33 changes: 30 additions & 3 deletions _examples/_template-webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,36 @@ The server listens on the port specified in `.env` (default 3000) and exposes:

## Configure Ory

1. In the Ory Console, configure an Action hook using the snippet in [`ory-actions.yaml`](ory-actions.yaml).
2. The body template is [`jsonnet/identity.jsonnet`](jsonnet/identity.jsonnet) — adjust as needed.
3. Set `ORY_WEBHOOK_SECRET` in the handler's `.env` to match the `X-Webhook-Secret` value declared in the Ory hook config.
1. Configure an Action hook using the snippet in [`ory-actions.yaml`](ory-actions.yaml). Attach `after` actions of the registration, login, and settings flows to the method-specific lists, not to the flow-level `after.hooks` lists:
- Registration: `selfservice.flows.registration.after.<method>.hooks` for each sign-up method you enable, such as `password` and `oidc`. On Ory Network, the `password` list contains `session`; keep it last.
- Login: `selfservice.flows.login.after.<method>.hooks` for each sign-in method.
- Profile updates: `selfservice.flows.settings.after.profile.hooks`.

Recovery and verification have only flow-level lists, such as `selfservice.flows.verification.after.hooks`. See [Attach `after` actions to authentication methods](https://www.ory.com/docs/kratos/hooks/configure-hooks#attach-after-actions-to-authentication-methods).
2. The body template is [`jsonnet/identity.jsonnet`](jsonnet/identity.jsonnet) — adjust as needed. Ory Network doesn't accept `file://` bodies, so set `body` to `base64://` followed by the Base64-encoded template. Read optional fields with `std.get`: Jsonnet fails on a missing field, and Ory then drops the request or fails the flow. The webhook context never contains `metadata_admin`, and its request headers don't include `X-Forwarded-For`; read the client IP address from `True-Client-Ip`.
3. Set `ORY_WEBHOOK_SECRET` in the handler's `.env` to match the `X-Webhook-Secret` value declared in the Ory hook config. Ory webhooks authenticate only with `api_key` or `basic_auth`.

## Webhook behavior

- `response.ignore: true` sends the request in the background. Ory doesn't wait for the handler and ignores its response. Use it for every webhook that only notifies another system.
- Without `response.ignore` or `response.parse`, a registration or settings webhook runs after the identity is saved. A response with status `400` or higher still fails the user's request, even though the change is saved.
- With `response.parse: true`, a registration or settings webhook runs before the identity is saved. In registration, `identity.id` is `00000000-0000-0000-0000-000000000000` at that point, so don't use it as a key in other systems. A `200` response with an `identity` object changes the identity; each field you return replaces the whole field, for example all of `metadata_public`. In the login flow, those changes apply only to that login's response and aren't saved.
- To block the flow and tell the user why, set `response.parse: true` and respond with a `4xx` status other than `429`, with this body:

```json
{
"messages": [
{
"instance_ptr": "#/traits/email",
"messages": [{ "id": 10000001, "text": "Only employees can sign up.", "type": "error", "context": {} }]
}
]
}
```

Use `#/` as `instance_ptr` for a message about the whole form. A `4xx` response without this shape fails the flow without showing the user a reason.
- Ory retries a request that can't be delivered, or that receives a `429` or `5xx` status other than `501`, for up to three attempts in total within a few seconds. All attempts for one trigger carry the same `Ory-Webhook-Trigger-Id` header.
- The handler must respond within about 30 seconds.

Detailed setup with screenshots: see the [docs page](https://ory.com/docs/integrates-with/).

Expand Down
16 changes: 10 additions & 6 deletions _examples/_template-webhook/jsonnet/identity.jsonnet
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Default body template: forward the identity payload to your webhook handler.
// Override this file when your handler needs a different shape.
// Default body template: forward the identity to your webhook handler.
// Override this file when your handler needs a different shape. Read optional
// fields with std.get: Jsonnet fails on a missing field, and Ory then drops
// the request. The webhook context never contains metadata_admin.
function(ctx) {
identity_id: ctx.identity.id,
email: ctx.identity.traits.email,
traits: ctx.identity.traits,
created_at: ctx.identity.created_at,
identity: {
id: ctx.identity.id,
traits: ctx.identity.traits,
created_at: ctx.identity.created_at,
},
client_ip: std.get(ctx.request_headers, 'True-Client-Ip', [null])[0],
}
51 changes: 37 additions & 14 deletions _examples/_template-webhook/ory-actions.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
hooks:
- hook: web_hook
config:
url: https://your-deployed-handler.example.com/<integration>/sync-user
method: POST
body: file://./jsonnet/identity.jsonnet
auth:
type: api_key
config:
name: X-Webhook-Secret
value: <your-shared-secret>
in: header
response:
ignore: true # Set to false + add `parse: true` if your handler returns identity metadata.
# Ory Network doesn't accept file:// bodies. Encode the template with
# `base64 < jsonnet/identity.jsonnet | tr -d '\n'` and paste it after base64://.
#
# response.ignore: true sends the request in the background. To change the
# identity or block the flow instead, replace it with response.parse: true and
# return the response shapes described in the README.

selfservice:
flows:
registration:
after:
password:
hooks: &registration_after_hooks
- hook: web_hook
config:
url: https://your-deployed-handler.example.com/<integration>/sync-user
method: POST
body: base64://<base64-encoded jsonnet/identity.jsonnet>
auth:
type: api_key
config:
name: X-Webhook-Secret
value: <your-shared-secret>
in: header
response:
ignore: true
- hook: session
oidc:
hooks: *registration_after_hooks
code:
hooks: *registration_after_hooks
passkey:
hooks: *registration_after_hooks
webauthn:
hooks: *registration_after_hooks
# The password list is aliased into the oidc, code, passkey, and webauthn
# lists. Alias it into any other method you enable, such as saml.
2 changes: 1 addition & 1 deletion _examples/_template-webhook/registry.entry.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ useCase: >-
Two to three sentences describing who this is for and why they would reach for this integration.
Concrete enough that a reader can self-identify whether it applies to them.
coreFunctionality: |-
- Endpoints exposed (sync vs async, can_interrupt, etc.) and what each one does.
- Endpoints exposed (async with response.ignore, or sync with response.parse) and what each one does.
- Auth mechanism (X-Webhook-Secret, HMAC for vendor callbacks).
- Identity-side state shape (metadata_public / metadata_admin keys).
- Non-obvious operational facts (rate limits, idempotency, retry behavior).
Expand Down
4 changes: 3 additions & 1 deletion _examples/_template-webhook/webhook/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ if (!ORY_WEBHOOK_SECRET) {
// Minimal shape of the body the Ory Action POSTs. Define a tighter type per
// integration once you know which jsonnet fields you actually need.
interface OryWebhookBody {
client_ip?: string | null;
identity?: {
id?: string;
traits?: {
Expand Down Expand Up @@ -83,7 +84,8 @@ app.post(
if (!vendorRes.ok) {
const detail = await vendorRes.text();
console.error(`Vendor API ${vendorRes.status}: ${detail}`);
// 5xx tells Ory to retry per the action's retry policy; 4xx would be terminal.
// Ory retries 429 and 5xx responses other than 501, up to three attempts
// in total within a few seconds. Other 4xx responses aren't retried.
res.status(502).json({ error: "sync_failed" });
return;
}
Expand Down
3 changes: 3 additions & 0 deletions cdp-analytics/amplitude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Configure an Ory Action on each lifecycle hook (async, `response.ignore: true`)

No webhook handler required — Amplitude's HTTP V2 API accepts authenticated POSTs directly.

Attach each action to the method-specific lists, such as `selfservice.flows.registration.after.password.hooks` and `selfservice.flows.login.after.password.hooks`, and repeat it for every method you enable, such as `oidc` or `code`. On Ory Network, the registration `password` list contains `session`; keep it last. Verification has only the flow-level `selfservice.flows.verification.after.hooks` list. See [Attach `after` actions to authentication methods](https://www.ory.com/docs/kratos/hooks/configure-hooks#attach-after-actions-to-authentication-methods).

## Setup outline

1. In Amplitude → **Settings** → **Projects** → select project → copy the **API Key** (server-side).
Expand All @@ -21,6 +23,7 @@ No webhook handler required — Amplitude's HTTP V2 API accepts authenticated PO
- Method: POST.
- Body Jsonnet: `{ "api_key": "<KEY>", "events": [{ "user_id": ctx.identity.id, "event_type": "ory_registration", "user_properties": { ... } }] }`.
- `response.ignore: true`.
- Read optional traits with `std.get`, because Jsonnet fails on a missing field and Ory then drops the request.
3. For EU data residency, use `https://api.eu.amplitude.com/2/httpapi`.

## Notable
Expand Down
6 changes: 3 additions & 3 deletions cdp-analytics/mailchimp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ Sync newly registered Ory Network users into a Mailchimp **audience** (subscribe
## How it works

1. User completes the registration self-service flow.
2. Ory triggers an `after` action registered on `flows.registration.after.hooks`.
2. Ory triggers the `after` action from the list of the sign-up method the user chose, for example `selfservice.flows.registration.after.password.hooks`.
3. The action evaluates a Jsonnet template that maps Ory identity traits to Mailchimp merge fields and tags.
4. Ory POSTs to the Mailchimp Marketing API to add or update the subscriber in the configured audience.
4. Ory POSTs to the Mailchimp Marketing API to add the subscriber to the configured audience. The webhook URL is static, so this action can only add new members. To update an existing member when the user changes their profile, point a settings action at your own handler, which finds the member and updates it.

## Setup

The full walkthrough lives in the [Ory docs page above](https://www.ory.com/docs/actions/integrations/mailchimp). Short version:

1. Create a Mailchimp **Marketing API key** (distinct from the Mandrill key used for transactional email).
2. Identify the target audience (list) ID from the Mailchimp dashboard.
3. Create an Ory Action of type `web_hook` on `flows.registration.after.hooks` pointing at `https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members`, with HTTP basic auth (`anystring:{api_key}`) and a Jsonnet body template that maps `identity.traits.email` and `identity.traits.name` into Mailchimp merge fields.
3. Create an Ory Action of type `web_hook` with `response.ignore: true` pointing at `https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members`, with `basic_auth` (`anystring:{api_key}`) and a Jsonnet body template that maps `identity.traits.email` and the name traits into Mailchimp merge fields. Read optional traits with `std.get`, because Jsonnet fails on a missing field and Ory then drops the request. Attach the action to `selfservice.flows.registration.after.<method>.hooks` for each sign-up method you enable, such as `password` and `oidc`, not to the flow-level `after.hooks` list. On Ory Network, the `password` list contains `session`; keep it last. See [Attach `after` actions to authentication methods](https://www.ory.com/docs/kratos/hooks/configure-hooks#attach-after-actions-to-authentication-methods).

## License

Expand Down
2 changes: 1 addition & 1 deletion cdp-analytics/mailchimp/registry.entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ useCase: >-
the right Mailchimp audience, tagged with traits collected at registration, without writing a separate sync
job or relying on manual exports.
coreFunctionality: |-
- Implemented as an Ory Actions webhook on `flows.registration.after.hooks` that calls Mailchimp's Lists/Audiences API.
- Implemented as an async Ory Actions webhook (response.ignore: true) in the registration method lists, for example `selfservice.flows.registration.after.password.hooks`, that calls Mailchimp's Lists/Audiences API to add members.
- Jsonnet template maps Ory identity traits (email, name, custom traits) into Mailchimp merge fields and audience tags.
- Synchronous or asynchronous webhook execution depending on whether registration should block on the Mailchimp call.
- Authentication via Mailchimp Marketing API key (separate from the Mandrill API key used by the transactional integration).
Expand Down
8 changes: 5 additions & 3 deletions cdp-analytics/mixpanel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@

## How it works

Configure an Ory Action on each lifecycle hook (async, `response.ignore: true`) with a Jsonnet body that emits a Mixpanel event payload, POSTed directly to `https://api.mixpanel.com/track` (or `https://api-eu.mixpanel.com/track` for EU residency).
Configure an Ory Action on each lifecycle hook (async, `response.ignore: true`) with a Jsonnet body that emits a Mixpanel event payload, POSTed directly to `https://api.mixpanel.com/track` (or `https://api-eu.mixpanel.com/track` for EU residency). The Project Token in the event payload authenticates the request.

Attach each action to the method-specific lists, such as `selfservice.flows.registration.after.password.hooks` and `selfservice.flows.login.after.password.hooks`, and repeat it for every method you enable, such as `oidc` or `code`. On Ory Network, the registration `password` list contains `session`; keep it last. Verification has only the flow-level `selfservice.flows.verification.after.hooks` list. See [Attach `after` actions to authentication methods](https://www.ory.com/docs/kratos/hooks/configure-hooks#attach-after-actions-to-authentication-methods).

## Setup outline

1. In Mixpanel → **Project Settings** → copy the **Project Token**.
2. Configure Ory Actions for each event:
- URL: `https://api.mixpanel.com/track?ip=0`.
- Method: POST with `Content-Type: application/json`.
- Method: POST. Ory sends the body as `application/json`.
- Body Jsonnet: `[{ "event": "ory_login", "properties": { "token": "<PROJECT_TOKEN>", "distinct_id": ctx.identity.id, "$insert_id": ctx.flow.id, ... } }]`.
- `response.ignore: true`.
3. For user-property updates (identity trait changes), use Mixpanel's `/engage` endpoint with `$set` operations.
Expand All @@ -26,7 +28,7 @@ Configure an Ory Action on each lifecycle hook (async, `response.ignore: true`)
- **Stable `distinct_id`** — use Ory's identity id, not email. Mixpanel identity merging breaks if `distinct_id` changes.
- **`$insert_id`** for deduplication — set to Ory's flow id so retries don't double-count events.
- Mixpanel's IP geolocation defaults to enabled (`ip=1`); pass `?ip=0` to disable when the IP belongs to your server, not the user.
- For **Service Accounts** (preferred over Project Token for server-to-server): use HTTP Basic auth with the service-account username/password.
- `/track` accepts only the Project Token. Service Account authentication (HTTP Basic) works with the `/import` endpoint, not with `/track`.

## Status

Expand Down
4 changes: 2 additions & 2 deletions cdp-analytics/mixpanel/registry.entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ type: config
maintainedBy: Community contributors
description: >-
Send Ory authentication events into Mixpanel (product analytics) via the Ingestion API. Ory Action POSTs
directly to Mixpanel's /track endpoint — no webhook handler needed; Project Token (or Service Account)
directly to Mixpanel's /track endpoint — no webhook handler needed; the Project Token in the payload
authenticates.
useCase: >-
A consumer product wants identity events (registration / login / verification / MFA) in Mixpanel funnels
and retention dashboards alongside in-product events — without standing up a webhook handler.
coreFunctionality: |-
- Pattern: Ory Action async POSTs directly to https://api.mixpanel.com/track?ip=0 (or api-eu.mixpanel.com for EU residency) — no handler in between.
- Authentication: Project Token in the event payload (properties.token), OR HTTP Basic with Service Account credentials (preferred for server-to-server).
- Authentication: Project Token in the event payload (properties.token). Service Account (HTTP Basic) authentication works with /import, not /track.
- Stable distinct_id: use Ory's identity id, NOT email — Mixpanel identity merges break if distinct_id changes.
- $insert_id for deduplication: set to Ory's flow id so retries don't double-count events.
- ip=0 disables Mixpanel's IP geolocation when the request comes from your server, not the user.
Expand Down
8 changes: 5 additions & 3 deletions cdp-analytics/mparticle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@

Configure an Ory Action on each lifecycle hook (async, `response.ignore: true`) with a Jsonnet body that emits an mParticle Events API payload, POSTed directly to `https://s2s.mparticle.com/v2/events` with HTTP Basic auth (mParticle API Key + Secret).

Attach each action to the method-specific lists, such as `selfservice.flows.registration.after.password.hooks` and `selfservice.flows.login.after.password.hooks`, and repeat it for every method you enable, such as `oidc` or `code`. On Ory Network, the registration `password` list contains `session`; keep it last. Verification has only the flow-level `selfservice.flows.verification.after.hooks` list. See [Attach `after` actions to authentication methods](https://www.ory.com/docs/kratos/hooks/configure-hooks#attach-after-actions-to-authentication-methods).

## Setup outline

1. In mParticle → **Setup** → **Inputs** → create a **Custom Feed** (Platform: Custom). Copy the **API Key** and **API Secret**.
2. Configure Ory Actions for each event:
- URL: `https://s2s.mparticle.com/v2/events`.
- Method: POST with `Content-Type: application/json`.
- Auth: HTTP Basic with `API Key:API Secret`.
- Body Jsonnet: `{ "events": [{ "event_type": "custom_event", "data": { "event_name": "ory_registration", "custom_event_type": "other" } }], "user_identities": { "customer_id": ctx.identity.id, "email": ctx.identity.traits.email }, "environment": "production" }`.
- Method: POST. Ory sends the body as `application/json`.
- Auth: `basic_auth` with `user` set to the API Key and `password` set to the API Secret.
- Body Jsonnet: `{ "events": [{ "event_type": "custom_event", "data": { "event_name": "ory_registration", "custom_event_type": "other" } }], "user_identities": { "customer_id": ctx.identity.id, "email": std.get(ctx.identity.traits, 'email', null) }, "environment": "production" }`.
- `response.ignore: true`.
3. Configure outputs in mParticle to route Ory events to whichever downstream destinations you use.

Expand Down
Loading
Loading