Skip to main content

Trigger custom logic and integrate with external systems with webhooks

Ory Actions supports webhooks, which are HTTP callbacks that can be triggered by specific events in your Ory-powered application. Webhooks allow Ory Actions to notify external systems, such as Hubspot, Mailchimp, when certain events occur, such as a user registration or profile update. Aside from integrating with external systems, webhooks allow you to use custom, external business logic in Ory Actions.

Should you use webhooks? They work perfectly for use cases like these:

Integration with third-party systems

If you want to integrate your Ory Actions-powered application with another system, such as a CRM, an email marketing platform, or an analytics tool, you can use webhooks to trigger updates in that system when events occur in your Ory Actions application.

Automated workflows

You can use webhooks to automate workflows and processes that span multiple systems. For example, you can use webhooks to automatically add a new lead to your CRM when a user registers in your application.

User data modification

You can use webhooks to modify user data when certain events occur. For example, you can use webhooks to update user data in your application when a user updates their profile information in another system.

Passing additional data on registration

When building your own UI you can pass additional data when completing the registration flow that gets forwarded to any configured webhooks. This can be used to solve more complex use cases that would be out of scope for identity traits.

tip

Always put security first! When using webhooks, ensure that the data you send is secure and that the external system you are integrating with is trustworthy. Additionally, make sure to consider the data privacy laws and regulations that may apply to your use case.

Configuration overview

Ory Action webhooks have several configuration options:

hook: web_hook # To use webhooks, you must set 'hook' to 'web_hook'
config:
url: https://test.hook.site.sh/before_login_hook # Webhook URL.
method: POST # HTTP method used to send request to the webhook URL.
body: base64://ENCODED_JSONNET # Encoded Jsonnet template used to render payload.
response:
ignore: false # Defines if the webhook response should be ignored and run async. Boolean. OPTIONAL
parse: false # Defines if the webhook response should be parsed and interpreted. Boolean. OPTIONAL
auth:
type: # Can be one of 'basic_auth' or 'api_key'
config: # Additional auth config for the hook. Read next section for details.
Expand to learn how to configure webhooks in your Ory Network project.

Follow these steps to start using webhooks in your project.

  1. Get your project configuration using Ory CLI:
ory get identity-config {project-id} \
--format yaml > project-configuration.yaml
  1. Add the webhook to your project configuration file:
project-configuration.yaml
session:
lifespan: # ...
# ...
selfservice:
flows:
registration: # or login, recovery, verification, settings
after:
hooks: [] # These acions are executed only if no more specific hooks in the `password` or `oidc` sections are defined
password:
# Actions in this section will be executed after registration (or login, settings) with a password
hooks:
- hook: web_hook
config:
url: https://webhook.target/example
method: POST
body: base64://ey...
response:
ignore: false
parse: false
auth:
# ...
- hook: session # see https://www.ory.sh/docs/actions/session
oidc:
hooks:
# Actions in this section will be executed after registration (or login, settings) with a social sign-in provider / OpenID Connect
# ...
  1. Update your project's configuration using the file you worked with:
ory update identity-config {project-id} \
--file project-configuration.yaml

Define HTTP request

Webhooks trigger HTTP requests to the webhook URL. You can configure the request method, URL, authorization, and body.

Customizing request body with Jsonnet

Webhooks bind the flow, as well as request headers (request_headers), request method (request_method), and the request URL (request_url) of the flow into the Jsonnet template for all methods and execution paths (before and after). For the after execution path of all flows, it binds the identity and the transient_payload object into the Jsonnet template as well. These objects are available through a ctx object.

To send { user_id: {some-id} } in the request body, create the following the Jsonnet template:

function(ctx) { user_id: ctx.identity.id }
Expand to see an example of all properties of the ctx object for a registration flow.
{
"ctx": {
"flow": {
"expires_at": "2023-01-31T12:19:35.782238Z",
"id": "cec1c06e-48eb-4f9d-abf1-2e287371f4eb",
"issued_at": "2023-01-31T11:19:35.782238Z",
"oauth2_login_challenge": null,
"request_url": "https://playground.projects.oryapis.com/self-service/registration/browser?return_to=",
"transient_payload": {
"custom_data": "test"
},
"type": "browser",
"ui": {
"action": "http://localhost:4455/self-service/registration?flow=cec1c06e-48eb-4f9d-abf1-2e287371f4eb",
"method": "POST",
"nodes": [
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden",
"value": "P91A1RzvL4xHAls2Gl76cbaXVMhBdpAj3c4vaRMckYY7JmGswmBHuul/+mZguOsQUOBmeJMOJWoa5xY2bd81CQ=="
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070002,
"text": "Your E-Mail",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"context": {},
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]
}
},
"identity": {
"created_at": "0001-01-01T00:00:00Z",
"id": "3a5293f1-f4d6-49f4-b34f-6da62c360604",
"metadata_public": null,
"recovery_addresses": [
{
"created_at": "0001-01-01T00:00:00Z",
"id": "00000000-0000-0000-0000-000000000000",
"updated_at": "0001-01-01T00:00:00Z",
"value": "0.g5vv0qpoxl@ory.sh",
"via": "email"
}
],
"schema_id": "default",
"schema_url": "",
"state": "active",
"state_changed_at": "2023-01-31T11:19:37.096674Z",
"traits": {
"email": "0.g5vv0qpoxl@ory.sh"
},
"updated_at": "0001-01-01T00:00:00Z",
"verifiable_addresses": [
{
"created_at": "0001-01-01T00:00:00Z",
"id": "00000000-0000-0000-0000-000000000000",
"status": "pending",
"updated_at": "0001-01-01T00:00:00Z",
"value": "0.g5vv0qpoxl@ory.sh",
"verified": false,
"via": "email"
}
]
},
"request_cookies": {
"__cypress.initial": "true",
"csrf_token_9eaa18484d26154fc69bb6b144100bec9a99a10709387620e8e4f0b7395e97ec": "BPshed6PaDaufaFQeuYRYeZ3MrDSeLVJxyk5X37DpI8="
},
"request_headers": {
"Accept": [
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
],
"Accept-Encoding": ["gzip"],
"Accept-Language": ["en-GB,en-US;q=0.9,en;q=0.8"],
"Cache-Control": ["max-age=0"],
"Connection": ["keep-alive"],
"Content-Length": ["180"],
"Content-Type": ["application/x-www-form-urlencoded"],
"Cookie": [
"csrf_token_9eaa18484d26154fc69bb6b144100bec9a99a10709387620e8e4f0b7395e97ec=BPshed6PaDaufaFQeuYRYeZ3MrDSeLVJxyk5X37DpI8=; __cypress.initial=true"
],
"Origin": ["http://localhost:4455"],
"Proxy-Connection": ["keep-alive"],
"Referer": ["http://localhost:4455/registration?flow=cec1c06e-48eb-4f9d-abf1-2e287371f4eb"],
"Sec-Ch-Ua": ["\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""],
"Sec-Ch-Ua-Mobile": ["?0"],
"Sec-Ch-Ua-Platform": ["\"macOS\""],
"Sec-Fetch-Dest": ["iframe"],
"Sec-Fetch-Mode": ["navigate"],
"Sec-Fetch-Site": ["same-origin"],
"Upgrade-Insecure-Requests": ["1"],
"User-Agent": [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
]
},
"request_method": "POST",
"request_url": "https://playground.projects.oryapis.com/self-service/registration?flow=cec1c06e-48eb-4f9d-abf1-2e287371f4eb"
}
}

Use the ctx object to access these fields in your Jsonnet template, for example:

./segment_identify.jsonnet
function(ctx) {
userId: ctx.identity.id,
customData: ctx.transient_payload.custom_data
traits: {
email: ctx.identity.traits.email,
name: ctx.identity.traits.name,
newsletterConsent: ctx.identity.traits.consent.newsletter,
},
}

Request authentication

Webhooks can be configured to use HTTP Basic Authentication or API Key authentication.

  • API key authentication. Type must be set to api_key. All properties are mandatory.

    hook: web_hook
    # other configuration keys
    config:
    auth:
    type: api_key
    config:
    name: Authorization
    value: { API Key value }
    in: header # alternatively "cookie"
  • "Basic Authentication" type authentication. Type must be set to basic_auth. All properties are mandatory. For basic_auth the config looks as follows:

    hook: web_hook
    # other configuration keys
    config:
    auth:
    type: basic_auth
    config:
    user: { USERNAME }
    password: { YOUR-PASSWORD }

Canceling webhooks

You can cancel configured webhooks and not make the defined request.

This can come in handy in testing. For example, by canceling a webhook, you prevent the test accounts created during testing from being added to CRM.

To get this behavior, cancel the webhook by raising an error for all accounts with the test- name prefix:

function(ctx)
if std.startsWith(ctx.identity.traits.email, "test-") then
error "cancel"
else
{
user_id: ctx.identity.id
}
info

Currently, Jsonnet doesn't support regular expressions. Follow this issue to see if the feature has been implemented: google/go-jsonnet/409.

Webhook response handling

Webhooks can be configured to ignore the response, or to parse the response and use it to interrupt the flow or to update the identity.

Non-blocking / async webhooks

Sometimes you need to interact with external systems without needing their response. For example, when a user signs up, a webhook is that adds their email address to the newsletter is triggered.

If you decide that the system response that indicates if the process was successful is not critical for your setup, make the webhook execution non-blocking to ignore whatever response is returned.

To do that, set response.ignore to true in the webhook config:

hook: web_hook
config:
response:
ignore: true

Modify identities

If you want the result of webhook execution to modify the identity, you can configure the webhook to parse the response:

hook: web_hook
config:
response:
parse: true

When the webhook target returns a 200 OK response code and the response body is a JSON object with the key identity in it, the values from that object will be used to change the identity before it is saved to the database.

info

Modifying the identity is currently only possible during the registration and settings flows.

When updating any of the identity fields, be aware that the whole field is replaced by the value returned by the webhook. For example, if the user signed up with

Identity before hook
{
"identity": {
"traits": {
"email": "joe@example.org"
}
}
}

and the webhook returns

Incorrect webhook response
{
"identity": {
"traits": {
"another_value": "example"
}
}
}

the identity will no longer have the email trait, which will break the sign up flow. Always make sure that the webhook returns complete data:

Correct webhook response
{
"identity": {
"traits": {
"email": "joe@example.org",
"another_value": "example"
}
}
}

Update identity traits

When the webhook target returns a 200 OK response code and the response body is a JSON object with the key identity.traits in it, the values from that object will be used to replace the identity traits before they are saved to the database.

info

This method replaces the entire identity traits object. It is not possible to update only a single trait.

{
"identity": {
"traits": {
"email": "0.g5vv0qpoxl@ory.sh",
"the_webhook": "updated this value"
}
}
}

Update identity metadata

When the webhook target returns a 200 OK response code and the response body is a JSON object with the key identity.metadata_public or identity.metadata_admin in it, the values from that object will be used to replace the identity metadata fields before they are saved to the database.

info

This method replaces the entire metadata object. You can't update only a single value in metadata_public or metadata_admin.

{
"identity": {
"metadata_public": {
"the_webhook": "changed this value",
"and_added_this_one": "too"
},
"metadata_admin": {
"the_webhook": "updated this value",
"and_this_one": "too"
}
}
}

Update verification or recovery addresses

When the webhook target returns a 200 OK response code and the response body is a JSON object with the key identity.verifiable_addresses in it, the values from that object will be used to replace the identity verification addresses before they are saved to the database.

info

Verification and recovery addresses are taken from the identity.traits object using the Identity Schema. If you add additional verification or recovery addresses, these will be deleted unless the Identity traits also contain that address.

{
identity: {
traits: {
email: "john@example.org",
},
verifiable_addresses: [
{
status: "completed",
value: "john@example.org",
verified: true,
via: "email",
},
],
recovery_addresses: [
{
value: "john@example.org",
via: "email",
},
],
},
}

Flow-interrupting webhooks

If you want the result of webhook execution to have the potential of interrupting the flow, you can configure the webhook to parse the response:

hook: web_hook
config:
response:
parse: true

The external service called in the flow decides if it allows the flow to continue, or if it interrupts the flow. For example:

  • For HTTP response codes 1xx, 2xx, and 3xx, the flow is not interrupted.
  • For HTTP response codes 4xx, 5xx, the external service interrupts the flow and returns a predefined payload.

Example payload:

{
"messages": [
{
"instance_ptr": "#/traits/foo/bar", # Indicates the field where there is an error.
"messages": [
{
"id": 123, # Unique numeric ID of the error that helps the frontend to interpret this message.
"text": "field must be longer than 12 characters",
"type": "validation",
"context": {
"value": "short value"
}
}
]
}
]
}

When using an after registration hook, you can define the specific point in the flow where this webhook is called:

  • When the webhook parses the response, the logic is called before the system creates a new identity.
  • When the webhook does not parse the response, the logic is called after the system creates a new identity.

You can call a webhook before and after the system creates an identity. To do that, define two webhooks with different parse values:

- hook: web_hook
config:
response:
parse: true
- hook: web_hook
config:
response:
parse: false
tip

Flow-interrupting webhooks work best if you give users meaningful information about the status of their flow. The best place to trigger these hooks is after flows.