Webhooks allow you to receive notifications when the risk of a device:

  • exceeds the Suspicious risk level (60), resulting in a $review.opened webhook.
  • exceeds the Malicious risk level (90), or a user tracks $review.escalated, resulting in a $incident.confirmed webhook.

A $review.opened or $incident.confirmed webhook will each be triggered at most once per user and device combination.

Example Review webhook

  "api_version": "v1",
  "app_id": "12345678901234",
  "type": "$review.opened",
  "created_at": "2016-10-02T00:30:08.276Z",
  "data": {
    "id": "xh6eyJxQ3AqouYqnACWYy2pe9NvLp59W",
    "user_id": "joH8NAVsYwRW4yBvPddS6TzydMmTjuzF",
    "trigger": "$login.succeeded",
    "context": {
      "ip": "",
      "user_agent": {
        "raw": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_9_5) ...",
        "browser": "Chrome",
        "mobile": false,
        "os": "Mac OS X 10.9.5",
        "version": "42.0.2311",
        "platform": "Mac OS X",
        "device": "Unknown",
        "family": "Chrome"
      "location": {
        "country_code": "US",
        "country":  "United States",
        "region_code": "CA",
        "region": "California",
        "lon": -122.3870544,
        "lat": 37.8019832
      "isp": {
        "isp_name": "CastleNet",
        "isp_organization": "Castle"

The payload for $incident.confirmed is identical to $review.opened except for the type field.

Receiving a webhook notification

Configuring your server to receive a new webhook is no different from creating any page on your website. With PHP, you might create a new .php file on your server; with a framework like Sinatra, you would add a new route with the desired URL.

Try out the webhook functionality before starting the implementation:

  1. Use a service like RequestBin to set up an ephemeral endpoint
  2. Head over to the Castle notification settings, enter the RequestBin URL in the webhook settings and click Test
  3. Watch the webhook appear in RequestBin

Now let's enter your own environment. Create the endpoint and add code to parse the JSON and start the user review process:

require "json"

# Using Sinatra
post "/my/webhook/url" do
  # Retrieve the request's body and parse it as JSON
  payload = JSON.parse(request.body.read)

  # Notify the user about risky activity
  if payload['type'] == '$review.opened'

  status 200
// Retrieve the request's body and parse it as JSON
$input = @file_get_contents("php://input");
$json = json_decode($input);

// Do something with $json

http_response_code(200); // PHP 5.4 or greater

Check out roll-your-own security notifications for a complete walk-through on how to build an end-to-end security flow based on webhooks..

Securing webhooks

Since anyone could in principle send webhooks to your application, it’s important to verify that these webhooks originated from Castle. Valid webhooks will therefore contain the header X-Castle-Signature which points to a HMAC SHA256 signature of the webhook payload (body):

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::Digest.new('sha256')
    calculated_hmac =
    calculated_hmac == hmac_header

# Respond to HTTP POST requests sent to this web service
post '/' do
  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}"
function verify_webhook($data, $hmac_header)
  $calculated_hmac = base64_encode(hash_hmac('sha256',
  return ($hmac_header == $calculated_hmac);

$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header);
error_log('Webhook verified: '.var_export($verified, true));