Skip to main content

Guide: Webhooks

This guide explains how to hook synchronous events or receive asynchronous ones using webhooks.

What Are Webhooks?

A webhook is an API endpoint (usually HTTP) that can be called by a third party in response to some particular trigger. Webhooks are therefore a way to be notified when something occurs, so your application can act on it accordingly.

Webhooks With SlashID

You can use the SlashID API to register webhooks for your organization. You can use webhooks to be notified when triggers occur within SlashID - for example, when a user fails to authenticate on your frontend. The webhook request will contain information about the trigger, as well as information about the webhook that you can use to verify the incoming request.

Creating a Webhook

You can use the SlashID webhooks API to create and manage webhooks. Each webhook has a target URL, which is the endpoint that will be called in response to a trigger. This URL must be an HTTPS URL, and must have a non-empty host that is neither localhost nor an IP address. It may not contain any query parameters or fragments. It must be unique amongst all of your organization's webhooks.

You can also specify custom headers per webhook that will be sent with each request. These can be used to include additional information in the request, such as authorization. These are encrypted at rest.

When creating a webhook, you may specify a timeout. This is the maximum amount of time that the SlashID client will wait for a response from your application after sending a webhook request. If your application fails to respond within the timeout, the call will be regarded as having failed. If you do not set a timeout when creating a webhook, default timeouts will be used. For event triggers, the default is 10 seconds. For synchronous hook triggers, the default is 2 seconds.

When you create a webhook, it has no associated triggers, and so will never be called. You can add triggers using the create trigger endpoint. A trigger is specified by a trigger type and trigger name. The trigger name indicates which specific trigger of the given type should cause the webhook to be called. For example, the trigger

{
"trigger_type": "event",
"trigger_name": "AuthenticationSucceeded_v1"
}

indicates that the webhook should fire on the event AuthenticationSucceeded_v1. Each webhook can have multiple triggers, and multiple webhooks can have the same trigger. Creating the same trigger twice for a single webhook has no effect.

Note that some triggers may fire frequently, and so you should consider which triggers you associate with webhooks, and whether rate limiting or other controls are required.

If you have multiple webhooks for a single trigger, they will be called in parallel, and there is no guarantee on the order in which they are called.

SlashID supports the following trigger types:

  • event
  • sync_hook

For event triggers, the following trigger names are supported:

  • AuthenticationSucceeded_v1
  • AuthenticationFailed_v1
  • PersonCreated_v1
  • PersonDeleted_v1
  • PersonIdentified_v1
  • PersonLoggedOut_v1
  • VirtualPageLoaded_v1

For synchronous hook triggers, the following trigger names are supported:

  • token_minted

For more information on events in SlashID, see our events documentation. For more information on synchronous hooks in SlashID, see our synchronous hooks documentation.

Handling a Webhook Request

Once you have created a webhook and added triggers for it, you will start to receive requests to the webhook.

Requests will be HTTP POST requests with content type application/jwt, to the target URL specified. It will also contain the custom headers you set when creating the webhook. Your application must return a 2XX status code in the response for the webhook call to be treated as successful. Note that 1XX information codes and 3XX redirect codes are not accepted, and are not regarded as successful.

For event triggers, if your application returns a non-successful status code (greater than 299) or times out, the call will be retried according to an exponential backoff strategy. The initial backoff will be 1 minute, and the maximum backoff is 10 minutes. The call will be retried for up to 30 minutes before being dropped.

For synchronous hook triggers, if your application returns a non-successful status code (greater than 299) or times out, the call will be retried two more times at intervals of 100ms, for a total of 3 attempts at most. If the final call fails or times out, the webhook call will be regarded as having failed. This may affect the flow that is being hooked - for example, the token_minted synchronous hook occurs during authentication, and failure to call relevant webhooks will result in the user authentication failing.

Note that for event triggers, it is possible for the webhook to be called more than once for a single event. As such, it is recommended that your webhook handling be idempotent and/or include some deduplication mechanism. The sub field in the webhook payload can be used to deduplicate webhook calls if needed.

When handling a request to a webhook, it is essential that you verify the contents of the request before processing it further. With SlashID, we have made this simple by using the JSON Web Token (JWT) and JSON Web Key (JWK) standards. The body of the request to the webhook is a signed and encoded JWT (just like our authentication tokens). In order to verify it, you should first retrieve the verification key JSON Web Key Set (JWKS) for your organization using the API. (Note that this endpoint is rate-limited, so we recommend caching the verification key.) You can then use this key to verify the JWT signature, and decode the body.

The JWT body will contain the following claims that you can use to validate the contents of the request:

  • jwt_id: a UUID uniquely identifying this invocation of the webhook for this trigger
  • aud: your organization ID
  • sub: a unique identifier for the instance of the trigger (for example, the event ID)
  • issuer: the URL of the SlashID API that issued this token
  • issued_at: the time when the webhook request was made
  • expires_at: the expiry time of the request, after which the request should be rejected; set to 5 minutes after issued_at
  • webhook_id: the ID of the webhook that was triggered
  • target_url: the target URL of the webhook that was triggered
  • trigger_type: the type of trigger that caused this request
  • trigger_name: which trigger caused this request
  • trigger_content: the full content of the trigger

You should check at least the following:

  • the JWT signature is valid, using the verification key
  • the expiry time has not elapsed
  • the organization ID is your SlashID organization ID
  • the issuer is the same as the base path from which you retrieved the verification key
  • the target URL matches the URL handling the request

You may also check that the webhook ID and trigger are as expected.

The body does not contain the custom headers; it is expected that your application will be able to verify these independently, if needed.

The trigger content contains the full details of the trigger, which depends on the trigger type and name. For event triggers, the trigger content will be the full event payload - please refer to our events documentation for details. For synchronous hook triggers, the trigger content depends on the type of hook - please refer to our hooks documentation.

For event triggers, any response body from your webhook server will be ignored. For synchronous hook triggers, you may optionally return a response body that can affect the hooked flows - please refer to our hooks documentation for details.

Validating a SlashID webhook request

Below are example implementations of secure webhook request validation.

from fastapi import HTTPException, status
import jwt

def verify_webhook(token):
"""
Verifies JWT token from request data using JWKS from provided endpoint.

:param token: Incoming request object.
:return: Decoded token if valid.
:raises: HTTPException with status 401 if token is invalid.
"""
# Initialize JWKS client
jwks_client = jwt.PyJWKClient(
"https://api.slashid.com/organizations/webhooks/verification-jwks",
headers={"SlashID-OrgID": "<ORGANIZATION ID>"}
)

webhookURL = "https://my-service.com/sid/webhook"

try:
# Get unverified header from JWT
header = jwt.get_unverified_header(token)

# Fetch the signing key from JWKS using the 'kid' claim from the JWT header
key = jwks_client.get_signing_key(header["kid"]).key

# Decode the JWT using the fetched signing key
verified_token = jwt.decode(token, key, audience="<ORGANIZATION ID>", algorithms=["ES256"])

if verified_token['target_url'] != webhookURL:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)

# Return decoded token
return verified_token

except Exception as e:
# Raise exception if JWT is invalid
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)

Example

Let's go through a step-by-step example where we create a webhook, add a trigger, and then receive and verify a request. First, create a webhook with the API:

curl -X POST --location 'https://api.slashid.com/organizations/webhooks' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '{
"target_url": "https://api.example.com/slashid-webhooks/authn-failed",
"name": "authentication failed",
"description": "Webhook to receive notifications when someone fails to authenticate",
"custom_headers": {
"X-EXAMPLE": ["something"]
}
}'

{
"result": {
"custom_headers": {
"X-EXAMPLE": [
"something"
]
},
"description": "Webhook to receive notifications when someone fails to authenticate",
"id": "16475b49-2b12-78d7-9012-cfe0e174dcd3",
"name": "authentication failed",
"target_url": "https://api.example.com/slashid-webhooks/authn-failed"
}
}

Note that in the response we get an id field - we will use this for the next steps. Also note the target URL - it is an HTTPS URL, with a path but no query parameters or fragments.

Now we will add a trigger to this webhook so that it is called whenever SlashID publishes an "authentication failed" event, using the webhook ID we received in the response:

curl -X POST --location 'https://api.slashid.com/organizations/webhooks/16475b49-2b12-78d7-9012-cfe0e174dcd3/triggers' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '{
"trigger_type": "event",
"trigger_name": "AuthenticationFailed_v1"
}'

Our webhook is now ready to go. We can test this using the /test-events endpoint, which can be used to publish events for testing. We will publish just a single AuthenticationFailed_v1 event:

curl -X POST --location 'https://api.slashid.com/test-events' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '[
{
"event_name": "AuthenticationFailed_v1"
}
]'

Your application will then receive a POST request, which would be represented with cURL like so:

curl -X POST --location 'https://api.example.com/slashid-webhooks/authn-failed' \
--header 'X-EXAMPLE: something' \
--header 'Content-Type: application/jwt' \
--data 'eyJhbGciOi[...]JFUzIOLWcifQ.eyJhdWQiOiIwM[...]DDAwMDAiLCAiZXhwIjoxNjg1.ipfHZf9tSRbVT[...]hXOVZmZw0sKPIpOV'

The body is a signed and encoded webhook (here shortened for readability) - we can verify it using the verification JWKS which we retrieve using the SlashID API:

curl --location 'https://api.slashid.com/organizations/webhooks/verification-jwks' \
--header 'SlashID-OrgID: <ORGANIZATION ID>'

{
"keys": [
{
"alg": "ES256",
"crv": "P-256",
"key_ops": [
"verify"
],
"kid": "WOJ_pw",
"kty": "EC",
"use": "sig",
"x": "whqB4q7Jap4zxPr-dmdl7u3SsA7KrQ3aM",
"y": "Gc35SgXrAbImsiYLfl-M4hSgjGex22M"
}
]
}

As this is the public key, you do not need to provide the API key to retrieve the verification key.

You can use a library of your choice to parse the JWKS and use it to verify the JWT signature. The decoded JWT body:

{
"aud": "<ORGANIZATION ID>",
"exp": 1685463580,
"iat": 1685463280,
"iss": "https://api.slashid.com",
"jti": "9c206041-be4f-482b-a129-321498fd3343",
"sub": "68a850ca-b2ee-46ce-8592-410813037739",
"webhook_id": "0647620e-ae30-7d18-8800-cf6732b6b007",
"target_url": "https://api.example.com/slashid-webhooks/authn-failed",
"trigger_name": "AuthenticationFailed_v1",
"trigger_type": "event",
"trigger_content": {
"analytics_metadata": {
"analytics_correlation_id": "3b132de1-3b11-4546-a2fe-521287c4a592"
},
"authenticated_methods": ["email_link"],
"browser_metadata": {
"window_location": "https://example.com/login",
"user_agent": "mozilla"
},
"event_metadata": {
"is_test_event": true,
"event_id": "68a850ca-b2ee-46ce-8592-410813037739",
"event_name": "AuthenticationFailed_v1",
"event_type": "AuthenticationFailed",
"organization_id": "<ORGANIZATION ID>",
"source": 2,
"timestamp": "2023-05-30T16:14:40Z"
},
"failed_authn_method": "webauthn",
"failure_reason": "no platform authenticator available",
"person_id": "0647620e-b32f-727b-8004-3f29d8ec5520"
}
}

You can now check that the aud, exp, iss, and target_url fields are all as expected. Note that the sub claim is the same as the event_id in the trigger_content.

Having verified the request, you can now process the trigger content according to your application's needs. Note that because we created this event using the /test-events endpoint, the is_test_event field in trigger_content is set to true.