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.
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.
- Get your project configuration using Ory CLI:
ory get identity-config {project-id} \
--format yaml > project-configuration.yaml
- Add the webhook to your project configuration file:
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
# ...
- 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:
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. Forbasic_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
}
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.
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": {
"traits": {
"email": "joe@example.org"
}
}
}
and the webhook returns
{
"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:
{
"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.
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.public_metadata
or identity.admin_metadata
in it, the values from that object will be used to replace the
identity metadata fields before they are saved to the
database.
This method replaces the entire metadata object. You can't update only a single value in metadata_public
or metadata_admin
.
{
"identity": {
"public_metadata": {
"the_webhook": "changed this value",
"and_added_this_one": "too"
},
"admin_metadata": {
"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.
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
, and3xx
, 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
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.