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.
If you would like to configure IP allow list in your firewall setup for webhooks please read about our Ory Network IP Addresses
Creating Actions on the Ory Network
The Ory Network Console offers a simple and intuitive tool to manage your Actions.
Follow the steps below:
- Navigate to Authentication → Actions & Webhooks in the Ory Console. There, click on + Create new Action.
- Choose the flow type that best suits your need and determine the execution time. For the
afterphase of the login, registration, and settings flows you can also select one or more authentication methods that trigger the webhook. Provide the endpoint URL and HTTP method. - Click on Next. The ensuing screen allows you to establish authentication parameters and input request body details. To learn more about tailoring your request body, see the relevant section below.
- Once you have all adjusted settings, click on Create action. Your new Ory Action has now been created and is ready to operate.
A single webhook can target several authentication methods at once. The legacy Fallback option is deprecated: a fallback webhook only runs when no method-specific hook (built-in or webhook) exists for the flow, so prefer selecting explicit methods.
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.
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), the request URL
(request_url), and the caller's approximate location (location) 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.
Accessing request headers
Request headers are available via ctx.request_headers. By default, only the following headers are exposed:
Accept
Accept-Encoding
Accept-Language
Content-Length
Content-Type
Origin
Priority
Referer
Sec-Ch-Ua
Sec-Ch-Ua-Mobile
Sec-Ch-Ua-Platform
Sec-Fetch-Dest
Sec-Fetch-Mode
Sec-Fetch-Site
Sec-Fetch-User
True-Client-Ip
User-Agent
X-Forwarded-Host
Ory-Base-Url-Rewrite
Ory-Base-Url-Rewrite-Token
X-Ory-Original-Host
Ory-No-Custom-Domain-Redirect
Cf-Ipcountry
To customize allowed headers, use the actions.web_hook.header_allowlist configuration option.
Accessing the caller's location
The caller's approximate location is available via ctx.location:
| Field | Type | Example |
|---|---|---|
city | string | Munich |
region | string | BY |
country | string | DE |
latitude | number or null | 48.117634 |
longitude | number or null | 11.603917 |
ctx.location and all of its fields are always present, so you can read ctx.location.country without checking first. When Ory
can't determine a value, strings are empty and coordinates are null:
function(ctx) {
country: ctx.location.country,
// Guard before doing arithmetic: coordinates can be null.
coordinates: if ctx.location.latitude != null then [ctx.location.latitude, ctx.location.longitude],
}
std.prune removes null values, so a pruned payload drops latitude and longitude when they're unknown.
There are two ways to block a flow based on the location. Raise an error in the template, which fails the hook without calling the
webhook target. This example limits sign-ups to a bounding box around the DACH region. It reads only ctx.location and
ctx.flow, so it works on the registration before position, where ctx.identity isn't bound yet:
function(ctx)
local loc = ctx.location;
local inRegion =
loc.latitude != null && loc.longitude != null &&
loc.latitude >= 45.8 && loc.latitude <= 55.1 &&
loc.longitude >= 5.9 && loc.longitude <= 17.2;
if !inRegion then
error 'sign-up is not available near ' + (if loc.city != '' then loc.city else 'your location')
else
{ flow_id: ctx.flow.id, country: loc.country }
Decide deliberately what an unknown location means for your rule. This example rejects the flow when the coordinates are null,
which also rejects every caller on a deployment where nothing sets the geolocation headers. Invert the check to allow those
callers through instead.
Use any error message other than "cancel". Raising error "cancel" skips the webhook and lets the flow continue, as described
in Canceling webhooks.
Alternatively, send the location to your webhook target and let it decide. The target rejects the flow by responding with a 4xx
status and a message payload, as described in Flow-interrupting webhooks.
Unlike ctx.request_headers, ctx.location isn't filtered by the actions.web_hook.header_allowlist setting.
On the Ory Network, Ory resolves the location at the edge. You don't have to configure anything.
On self-hosted deployments, including the Ory Enterprise License, there is no such edge and Ory ships no geolocation database. Ory
reads the values from the Cf-* request headers, so filling them is your responsibility: the fields stay empty until a
reverse proxy in front of Ory sets them. Use a proxy that can resolve an IP address to a location, such as nginx with the
ngx_http_geoip2 module and a MaxMind database.
Expand to see an example nginx configuration.
geoip2 /etc/nginx/GeoLite2-City.mmdb {
$geoip2_city city names en;
$geoip2_subdivision_code subdivisions 0 iso_code;
$geoip2_country_code country iso_code;
$geoip2_latitude location latitude;
$geoip2_longitude location longitude;
}
server {
location / {
proxy_set_header Cf-Ipcity $geoip2_city;
proxy_set_header Cf-Region-Code $geoip2_subdivision_code;
proxy_set_header Cf-Ipcountry $geoip2_country_code;
proxy_set_header Cf-Iplatitude $geoip2_latitude;
proxy_set_header Cf-Iplongitude $geoip2_longitude;
proxy_pass http://kratos;
}
}
Set all five headers this way, including any you can't resolve. proxy_set_header replaces whatever the client sent, and nginx
omits a header whose value is empty, so a caller can't inject their own location. If you skip a header instead, the value the
client sent reaches Ory unchanged, and you must not use these fields for authorization decisions.
Jsonnet templating
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",
"via": "email"
}
],
"schema_id": "default",
"schema_url": "",
"state": "active",
"state_changed_at": "2023-01-31T11:19:37.096674Z",
"traits": {
},
"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",
"verified": false,
"via": "email"
}
]
},
"location": {
"city": "Munich",
"region": "BY",
"country": "DE",
"latitude": 48.117634,
"longitude": 11.603917
},
"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.flow.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 keysconfig:auth:type: api_keyconfig:name: Authorizationvalue: { API Key value }in: header # alternatively "cookie" -
"Basic Authentication" type authentication. Type must be set to
basic_auth. All properties are mandatory. Forbasic_auththe config looks as follows:hook: web_hook# other configuration keysconfig:auth:type: basic_authconfig: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
}
Jsonnet doesn't support regular expressions yet. 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 only
possible during 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": {
}
}
}
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": {
"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 isn't possible to update only a single trait.
{
"identity": {
"traits": {
"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. This method replaces the entire metadata object. Partial updates to values in metadata_public or metadata_admin
aren't supported.
{
"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.
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: {
},
verifiable_addresses: [
{
status: "completed",
verified: true,
via: "email",
},
],
recovery_addresses: [
{
via: "email",
},
],
},
}
Update the identity's external ID
When the webhook target returns a 200 OK response code and the response body is a JSON object with the key identity.external_id
in it, the value from that object will be used to replace the identity's external ID before it is saved to the database. This
method replaces the previous external ID, if any.
{
"identity": {
"external_id": "external-id-from-webhook"
}
}
Require a second factor
A webhook on the login after position can require the user to reach AAL2 for that sign-in. Use this when an external risk engine
decides that a login looks risky and should be confirmed with a second factor.
ctx.location is a common input for that decision — for example, stepping up a sign-in from a
country the account has never used before. Decide what an unresolvable location means for your rule: an empty country is not a
match for any country you compare it against, so a rule written as "step up unless the country is known-good" steps up, while
"step up only when the country is known-bad" doesn't.
This is off by default. Turn it on first under Project settings → Advanced in the Ory Console by enabling the
webhook_response_directives feature flag, or in the configuration:
feature_flags:
webhook_response_directives: true
The webhook must parse the response. Return required_aal with the value aal2:
{
"required_aal": "aal2"
}
What the user sees depends on what they have set up:
- A user with a second factor is sent to the two-factor login screen and finishes signing in there.
- A user without a second factor is sent to the settings flow to set one up. They are never let through without it.
You can update the identity in the same response:
{
"identity": {
"metadata_admin": { "risk": "high" }
},
"required_aal": "aal2"
}
aal2 is the only supported value — any other value fails the login rather than being quietly dropped. required_aal returned
from a webhook on another position is logged and ignored, because there is no login to require a second factor for. While the
feature flag is off, required_aal is ignored everywhere.
Require sign-in with a specific OpenID Connect provider
A webhook on the login after position can require the user to sign in again with one specific OpenID Connect provider. Use this
when an external check has to confirm who the user is before they can continue — for example a bank ID broker or a provider that
verifies government-issued IDs.
This uses the same feature flag as required_aal. Turn on the webhook_response_directives feature flag first:
- Ory Network
- Ory OEL
Go to Project settings → Advanced in the Ory Console and enable the webhook_response_directives feature flag.
Set the flag in the configuration:
feature_flags:
webhook_response_directives: true
The webhook must parse the response. Return force_credential with the method and the ID of the provider from your social sign-in
configuration:
{
"force_credential": {
"method": "oidc",
"provider": "my-oidc-provider"
}
}
oidc is the only supported method. Other methods, such as password, code, or totp, are not supported yet.
A force_credential value that names another method, or that has no provider, fails the login rather than being quietly dropped.
So does a provider that isn't configured, or that is scoped to an organization, or the oidc method being disabled: no sign-in
could ever satisfy that requirement, so the login fails with a configuration error instead of issuing a session that can't be
used. If two webhooks on the same position require different providers, the login fails with a configuration error that names both
webhooks — change the configuration so that at most one webhook requires a credential. force_credential returned from a webhook
on another position is logged and ignored, because there is no login to attach the requirement to. While the feature flag is off,
force_credential is ignored everywhere.
You can combine the directive with required_aal and update the identity in the same response. The two requirements are
independent, and the user has to satisfy both. Signing in with the required provider issues a new session that keeps the AAL
floor, so the user still has to complete the second factor:
{
"identity": {
"metadata_admin": { "risk": "high" }
},
"required_aal": "aal2",
"force_credential": {
"method": "oidc",
"provider": "my-oidc-provider"
}
}
What the user sees
The requirement is stored on the session, so it outlasts the login that created it. While it is pending, the session checks refuse
the session: /sessions/whoami, GET /sessions, and the settings flow respond with 403 Forbidden and the error ID
session_credential_required.
{
"error": {
"id": "session_credential_required",
"code": 403,
"details": {
"redirect_browser_to": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/browser?refresh=true"
}
}
}
If the session still owes a second factor, the session checks first answer with session_aal2_required instead, and the user
completes the second factor before the required sign-in. The required sign-in can link a new provider account to the identity, so
it takes the same Authenticator Assurance Level as linking an account in the settings flow
(selfservice.flows.settings.required_aal).
Send the user to the URL in error.details.redirect_browser_to. A browser request to the settings flow goes there on its own,
with a redirect instead of the error. That login flow shows only the required provider's sign-in button, with no password field
and no other provider. It is a refresh sign-in, so where the provider supports it, Ory sends prompt=login to ask the provider to
authenticate the user again rather than reuse the session the user may still have there. Whether the user re-authenticated is the
provider's decision, and Ory relies on the provider's answer.
The login the webhook ran in ends the same way:
- A browser is redirected to that login flow with a
303 See Other. - A single-page app receives
422 Unprocessable Entitywith the error IDbrowser_location_change_requiredand the URL to open. - A native app receives
200 OKwith a session token but without the identity. Its next call to/sessions/whoamireturns the 403 above.
Signing in with the required provider is the only thing that clears the requirement. Signing in with another credential — the password, for example — does not: the refreshed session keeps the requirement. If the webhook requires the provider the user has just signed in with, the requirement is already met and that login finishes normally.
The login flow is tied to the requirement it was created for. If that requirement is cleared or replaced, or the session is revoked, while the flow is open, completing the flow fails and the user starts the sign-in again.
Providers the identity hasn't used before
The user doesn't need an account at the provider yet. If the identity has never signed in with it, completing the required sign-in links that provider account to the identity, together with the tokens the provider issued, including the ID token. Read them with Get social sign-in provider tokens. This makes the directive work for providers that exist to assert something about the user, such as a verified legal name, rather than to sign them in.
If the identity already has an account at that provider, only that account satisfies the requirement. Signing in with a different account at the same provider fails the login, and so does an account that is already linked to another identity.
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 below 400, the flow continues unless processing the webhook response returns an error.
- For HTTP response codes
4xxother than 429, or501, the external service can interrupt the flow with validationmessagesusing the payload below. Responses with status429or other5xxcodes are retried; if retries are exhausted, the flow fails withwebhook_request_failedinstead of returning their validation messages.
Example payload:
{
"messages": [
{
"instance_ptr": "#/traits/foo/bar", // Points to the field that failed validation.
"messages": [
{
"id": 123, // Unique numeric ID of the error that helps the frontend to interpret this message, doesn't have to be unique.
"text": "field must be longer than 12 characters",
"type": "error", // One of "error", "info", "success".
"context": {
// Contextual information about the error. Is schemaless, and can be used to provide additional information to the frontend.
// Typically used to provide fields that are used in internationalization messages.
"value": "short value",
"any": "additional information"
}
}
]
}
]
}
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.
Webhook retries
Sometimes webhook delivery can fail due to issues such as network problems or server errors. To mitigate this, Ory Network implements a retry policy that attempts to deliver the payload up to three times, with a 30-second timeout between each attempt. This means that if delivery fails on the first attempt, Ory Network will automatically retry the delivery two more times, with a 30-second delay between each retry.
Webhook errors
For webhooks whose response isn't ignored, the following request failures and error-status responses produce an error.id:
| Error ID | HTTP status | Cause |
|---|---|---|
webhook_timeout | 504 | The webhook didn't respond in time. |
webhook_request_failed | 502 | The request couldn't be delivered, or retries were exhausted on 429 or a 5xx status other than 501. |
webhook_unexpected_status | 502 | The webhook responded with a 4xx status other than 429, or with 501, and response parsing is disabled or the body has no valid validation messages. |
If the webhook parses the response and responds with a 4xx status other than 429, or with 501, and valid messages, the flow
returns those messages as validation errors instead. See Flow-interrupting webhooks.
These IDs don't cover every webhook failure. For example, an empty or malformed HTTP 200 response with response.parse enabled
still fails without a webhook error ID.