If you signed up before June 3, 2021, see the migration guide to learn about the recent API changes.



Whenever Castle detects an increased risk in one of your user’s accounts, a security event is triggered and you can configure Castle to notify your application when these events occur using webhooks. In order to help you implement robust account recovery or user review automation workflows, Castle can be configured to send webhooks to your application based on two types of security events:

Security eventDescription
$review.openedThis event will be triggered whenever the event has status: $succeeded and the Castle response policy.action is challenge
$incident.confirmedThis event will be triggered whenever the event has status: $succeeded and the Castle response policy.action is deny

The policy.action is is defined in the Policy that triggered. This policy.action corresponds to the value that will be returned by the inline APIs calls.

A $review.opened or incident.confirmed webhook will each be triggered once per user and device combination. Webhooks are sent when a new threat has been detected for a user’s account.

To determine which policy triggered the webhook, you may check the data.policy details in the example webhook.


Refer to the Example Webhook at the end of this document.


Register a webhook endpoint

Whenever security events are triggered and webhooks are enabled in your Castle environment, Castle will send an HTTP POST request to your endpoint in JSON format. At minimum, the webhook endpoint needs to expect and handle an HTTP POST request and confirm successful receipt of the data by returning a 2xx HTTP status code.

Below are the key steps for successfully building out a webhook endpoint

HTTPS URL: If you are using a HTTPS URL for your webhook endpoint, Castle will validate that the connection is secure and your server must be correctly configured with a valid certificate.

Try out the Castle webhooks

Before starting the implementation, it can be useful to try out the webhooks functionality and get to know the data:

  1. Use a service like Webhook Tester to set up a temporary endpoint
  2. Head over to the webhooks settings page, and follow the steps for testing webhooks below
  3. Watch the webhook appear in the web interface

Parse the payload

The data in the webhook request sent by Castle will be formatted using JSON and this will be indicated by the Content-Type HTTP header which will be set to application/json . For a complete specification and example of the content of the request body, see the example webhook. Your code for handling webhooks is expected to parse the request body into an object.

Respond with a status code

To let Castle know that you have received the webhook successfully, your endpoint must return a 2xx HTTP status code. Codes that are not in this range, including 3xx codes, will be treated as unsuccessful by Castle.

4xx and 5xx HTTP status codes typically indicate an issue with your endpoint. 4xx indicates Castle sent a request to your server, but the endpoint was not valid, such as a 404 which indicates that the path doesn’t exist. If a 5xx code is returned this usually indicates an unhandled exception in your application code.

Retries: In case of failure, Castle will attempt to deliver your webhooks up to three additional times with exponential back off before giving up.

Test your webhook endpoint

From the webhooks settings page in the Castle Dashboard you can test that your endpoint works correctly. Remember to test your webhook endpoint often, and especially:

  • Upon Creation of a webhook endpoint
  • After deploying it to a live environment
  • After making any changes

Check the signature

This step is optional, but highly recommended. Since a webhook endpoint is a publicly facing URL, anyone could potentially send requests to it, pretending to be Castle, if they got hold of the URL. In order to protect from spoofing, Castle includes a signature header, X-Castle-Signature, in the request so that you can verify the authenticity of the request. The signature is a Hash-based Message Authentication Code using the SHA256 hashing function (HMAC-SHA256) and is computed with the raw JSON request body together with the API secret. To validate the request, simply compute the HMAC within the code that handles the webhook and compare it to the value sent in the signature header. The two values should match.

Example signature checking code:

require 'json'

# Using Sinatra
require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'

helpers do
  # Compare the computed HMAC digest based on the shared secret and the
  # request contents to the reported HMAC in the headers
  def verify_webhook(data, hmac_header)
    digest  ='sha256')
    calculated_hmac =
    calculated_hmac == hmac_header

# Respond to HTTP POST requests sent to this web service
post '/' do
  data =
  hmac_header = request.headers['X-Castle-Signature']
  verified = verify_webhook(data, hmac_header)

  # IMPLEMENT: handle the alert

  # Output 'true' or 'false'
  puts "Webhook verified: #{verified}"

Testing Webhooks

From the webhooks settings page, you can choose which security events to subscribe to. If you’re using the same webhook endpoint for dispatching all of your security workflows, you can choose to subscribe to all events.

The first step is to create a new webhook entry. You can create as many endpoints as you wish, eg. one per security event, or eg. separate endpoints for internal logging and sending out user notifications.

A screenshot showing the webhook registration form
Enter a valid, public URL for the webhook endpoint

Before enabling the webhook, make sure that the URL that you’ve entered is valid and publicly facing. It is also highly recommended that you test the webhook with a simulated event before enabling it:

A screenshot showing registered webhook endpoints
Registered webhook endpoints

After pressing the “test” button, you’ll get to select which security event to simulate:

A screenshot of the webhook testing feature
Testing a webhook endpoint

After you select the “Send test webhook” button, Castle will attempt to deliver a webhook to the specified endpoint, simulating the selected security event. If a non 2xx code was returned by your webhook endpoint an error message will show up, indicating that the attempt was not successful together with additional information from the response, which can help you to debug the problem.

Finally, when everything seems to be working as expected, hit the “edit” button and enable the webhook.

Example webhook

  "api_version": "v1",
  "app_id": "368721375658952",
  "type": "$incident.confirmed",
  "risk": 0.9878270039877164,
  "created_at": "2021-02-24T21:39:33.495Z",
  "data": {
    "id": "P2lbg8_UdnSTVmK0g2DeOjREKc91",
    "device_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlbiI6IlAybGJnOF9VZG5TVFZtSzBnMkRlT2pSRUtjOTEiLCJxdWFsaWZpZXIiOiJBUUlEQ2pFeE5ETTFOekUxTXpBIiwiYW5vbnltb3VzIjpmYWxzZSwidmVyc2lvbiI6MC4zfQ.cM_SgKgLMCRKAhkWJK5YxS3nXP5u47vEnmAD_vjLlI8",
    "user_id": "f2dbec55-95f3-4d7a-8d28-f73799f892ed",
    "trigger": "$login.succeeded",
    "context": {
      "ip": "",
      "isp": { "isp_name": "SK Broadband", "isp_organization": "SK Broadband" },
      "location": {
        "country_code": "KR",
        "country": "South Korea",
        "region": "Seoul",
        "region_code": "11",
        "city": "Songpa-dong",
        "lat": 37.5079,
        "lon": 127.1177
      "user_agent": {
        "raw": "Mozilla/5.0 (Windows; U; Windows NT 6.0) AppleWebKit/535.1.2 (KHTML, like Gecko) Chrome/24.0.832.0 Safari/535.1.2/807.831",
        "browser": "Chrome",
        "version": "24.0.832",
        "os": "Windows Vista",
        "mobile": false,
        "platform": "Windows Vista",
        "device": "Unknown",
        "family": "Chrome"
    "user_traits": { "email": "" },
    "properties": {},
    "policy": {
      "id": "e8878769-9851-4430-8029-23cb9237a62d",
      "revision_id": "40087135-83ba-4b38-9931-0981a3d023fd",
      "name": "Deny successful logins",
      "action": "deny",
      "type": "authentication"
Due to legacy support, the trigger field will be of the concatenated event format used by customers that signed up before June 3, 2021. For instance, if you send event: "$login" and status: "$succeeded" to the API, trigger will become $login.succeeded.