Webhooks

Subscribe to events so your integration can automatically trigger workflows.

Castle uses webhooks to notify your application whenever events happens in your Castle environment. Webhooks are useful to e.g. kick off account recovery workflows when risky behavior is detected, or whenever the contents of Lists changes.

Available webhook events

Security events

Whenever a Castle policy generates a non-allow result, 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 takeover workflows, Castle can be configured to send webhooks to your application based on two types of security events:

Event nameDescription
$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 defined in the Policy that triggered. This policy.action corresponds to the value that will be returned by the inline APIs calls.

By default, a $review.opened or incident.confirmed webhook will each be triggered once per user and device combination, however, if you need the webhooks to be sent every time a policy triggers, then please reach out to [email protected] and we will enable this functionality for you.

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

Example payload

{
  "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": "180.69.216.170",
      "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": "[email protected]" },
    "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.

List events

In order to get notified whenever changes happens in your Lists, the following events are available to subscribe to:

Event nameDescription
$list_item.createdA new item was added to a List. By default, new items are active and will match against incoming events.
$list_item.archivedAn item in a list was archived and is no longer active
$list_item.unarchivedA previously archived item was un-archived and is now active again

Example payload

{
  "api_version": "v1",
  "app_id": "967132239648643",
  "created_at": "2022-09-28T09:34:42.451Z",
  "type": "$list_item.archived",
  "data": {
    "id": "c485c3cf-a1c6-48db-903a-1b694c9c541d",
    "primary_value": "42138",
    "secondary_value": null,
    "auto_archives_at": "2022-09-28T09:34:42.443Z",
    "author": {
      "type": "$castle_dashboard_user",
      "identifier": "a700fcff-e4ee-48bc-8db8-8f2ec386d0c7",
      "details": {}
    },
    "comment": "Fraud!",
    "list": {
      "id": "96e5ed62-7774-4cd3-bfe6-c70921d47c50",
      "name": "Blocked users",
      "primary_field": "user.id",
      "secondary_field": null
    }
  }
}

Implementing Castle webhooks

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 an 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 four additional times with exponential back off before giving up. The retry logic is applied whenever a non 2xx code is returned

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  = OpenSSL::Digest.new('sha256')
    calculated_hmac =
      Base64.encode64(OpenSSL::HMAC.digest(
        digest,
        'YOUR_API_SECRET',
        data)
      ).strip
    calculated_hmac == hmac_header
  end
end

# Respond to HTTP POST requests sent to this web service
post '/' do
  request.body.rewind
  data = request.body.read
  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}"
end

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, e.g. one per security event, or eg. separate endpoints for internal logging and sending out user notifications.

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:

After pressing the "test" button, you'll get to select which security event to simulate:

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.


Allowlisting Castle IPs

For additional security, you may want to ensure that the webhooks hitting your endpoint are in fact from Castle.

Production webhooks will always be sent from the following set of IP Addresses:

34.194.74.182
23.20.75.190
34.194.26.104

Test webhooks can originate from any AWS EC2 IP. If you want to limit traffic to these IPs, you can use the following call to fetch the most recent list of all AWS EC2 IPs:

curl -s https://ip-ranges.amazonaws.com/ip-ranges.json | jq -r '.prefixes[] | select(.region=="us-east-1") | select(.service == "EC2").ip_prefix'