What are Webhooks?

Get notifications when an event occurs

A webhook is a mechanism that allows Samsara to send an HTTP request to your application when an event occurs. Webhooks exist in Samsara's product as part of two features:

  • Alert Webhooks (examples include geofence alerts and speeding alerts)
  • Event Subscriptions (examples include document submitted and vehicle updated)

To create a webhook you must:

  1. Define the webhook handler in your application
  2. Register the webhook handler with Samsara
  3. Configure Alerts or Event Subscriptions to send the webhook notifications

πŸ“˜

Submit Feedback

We are alway open to product feedback, whether it's more webhooks you want to see, enhancements to existing webhooks, or anything webhooks related! Submit feedback here.

Webhook Versions

Samsara has two major versions of webhook payloads: Webhooks 1.0 and Webhooks 2.0.

  • Webhooks 1.0 is our first version and has the essential information for objects returned.
  • Webhooks 2.0 was developed to exposed rich and full JSON via webhooks to improve the developer experience.

Samsara will no longer be adding capabilities to Webhooks 1.0, and will eventually deprecate them in favor of Webhooks 2.0. This deprecation date has not been decided on yet and will be communicated ahead of time, with enough time for you to migrate from 1.0 to 2.0 if needed.

Payload Body

The body of a webhook payload is a JSON object. All webhook notifications have the following fields:

FieldDescription
eventIdThe ID of the event. If you need to contact [email protected], this can be helpful in debugging the issue.
eventMsThe Unix epoch timestamp in milliseconds of when the event was sent by the Samsara server.
eventTypeValue is always Alert or Ping.
eventThe event that triggered the webhook notification. See below for details on different events.

For example:

{
  "eventId": "bf72239c-bebd-4c22-b64c-6fc692ef7c46",
  "eventMs": 1587597109477,
  "eventType": "Alert",
  "event": {...}
}

Helpful Tools for Getting Started

Here are a few helpful tools:

  • RequestBin and webhook.site - both of these allow you to inspect webhook notification HTTP payloads. They provide you with a URL and a UI to inspect any HTTP requests made to that URL. You can configure your webhook to use the testing URL provided by one of these tools and then inspect the notifications from Samsara.
  • curl and Postman - both of these tools allow you to send HTTP requests. This is helpful when you are testing your webhook handler. curl is a command-line tool and Postman is a GUI.
  • ngrok allows you to create a public URL that redirects to your localhost. This allows you to test your webhook locally without having to deploy it to a public server. Note: if the ngrok process terminates, the URL will not longer be usable.

Define the webhook handler

Your application should expose a URL endpoint that is capable of receiving an HTTP POST request from Samsara.

πŸ“˜

Webhook URLs must use HTTPS.

Sample Code

The following examples demonstrate how to receive a webhook notification from Samsara. See below for an example of a full webhook payload.

from flask import Flask, request
app = Flask(__name__)

# Using Flask
@app.route('/webhook-handler', methods=['POST'])
def webhook():
    # parse notification JSON
    notification = request.json

    if notification is None: # could not parse request
        return ('', 400)

    try:
        if notification['event']['alertConditionId'] == 'DeviceLocationInsideGeofence':
            # event is a geofence entry alert
            # do something with the alert info
        elif notification['event']['alertConditionId'] == 'DeviceSpeedAboveSpeedLimit':
            # event is a speeding alert
            # do something with the alert info
        # ... handle other types of alerts
    except ValueError as e:
        # request is malformed
        return ('', 400)
    
    return ('', 200)
// This example uses Express to receive webhooks
const app = require('express')();
const bodyParser = require('body-parser');

app.post('/webhook-handler', bodyParser.raw({type: 'application/json'}), (request, response) => {
  let notification;

  try {
    notification = JSON.parse(request.body);
  } catch (err) {
    response.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (notification.event.alertConditionId) {
    case 'DeviceLocationInsideGeofence':
      // The event is a geofence entry alert
      // Do something with the alert info
      break;
    case 'DeviceSpeedAboveSpeedLimit':
      // The event is a speeding alert
      // Do something with the alert info
      break;
    // ... handle other types of alerts
    default:
      // Got something unexpected
      return response.status(400).end();
  }

  // Return a response to acknowledge receipt of the event
  response.json({received: true});
});

app.listen(8000, () => console.log('Running on port 8000'));

Example Webhook Notification

Example webhook event (see Webhook Reference for full details):

POST /webhook-handler HTTP/1.1
Host: your-app.com
User-Agent: Samsara-Webhook/bf126598c478d8f4149f823a3651c44e3a268119
X-Samsara-Timestamp: 1587597109
X-Samsara-Signature: v1=b2d6e7f01a882a8861c2fee2fc00732dd79eaaadc67d0374b95f4bca252f2ec2
X-Samsara-Request: bf72239c-bebd-4c22-b64c-6fc692ef7c46
X-Samsara-Event-Type: Alert
Content-Type: application/json
Content-Length: 700

{
  "eventId": "bf72239c-bebd-4c22-b64c-6fc692ef7c46",
  "eventMs": 1587597109477,
  "eventType": "Alert",
  "event": {
    "alertEventUrl": "https://cloud.samsara.com/o/53729/alerts/incidents/v2/159827/1/212014918732717/1587596963539/link?dl=eyJkZWVwTGlua1R5cGUiOiJBbGVydENvbmRpdGlvblR5cGVfRGV2aWNlTG9jYXRpb25JbnNpZGVHZW9mZW5jZSIsImdyb3VwSWQiOjU0ODY4fQ==",
    "alertConditionDescription": "Vehicle is inside geofence",
    "alertConditionId": "DeviceLocationInsideGeofence",
    "details": "'Little Red' is inside Parent's House.",
    "device": {
      "id": 212014918732717,
      "name": "Little Red",
      "serial": "G9MTH7CNKZ",
      "vin": "JTMBK32V895081147"
    },
    "orgId": 53729,
    "resolved": false,
    "startMs": 1587596963539,
    "summary": "'Little Red' is inside Parent's House."
  }
}

Return a 2XX Status Code Quickly

Samsara expects your application to return a 2XX status code upon the successful receipt of the notification.

  • It's best to return a 2XX code as soon as you have verified the contents of the alert. Do longer-running processing asynchronously.

πŸ“˜

Samsara does not support redirects (status codes of 301, etc). You should always return a 2XX status code if you received the webhook successfully.

Webhook Retry

If your application returns anything other than a 2XX status code, Samsara will interpret this as an error and resend the notification using exponential backoff. Samsara will attempt to send the notification a total of 5 times, after which the notification will not be sent again. If a new alert fires, Samsara will attempt to send that new notification to the webhook.

❗️

We reserve the right to disable inactive webhooks. If your webhook handler never returns a 2xx, we may consider it inactive and disable it.

Register the webhook handler

Webhook handlers are configured in the Samsara dashboard. Go to your organization's Settings page, scroll down, and select Webhooks from the left-side navigation bar:

644

Click the "Add Webhook" button. Give your webhook a name and enter the URL of your application handler that receives the webhook notifications. Note that HTTPS is required. Click "Create" to finish creating the webhook:

1058

Custom Headers

You may include up to 5 custom headers when you register your webhook. When Samsara sends an event to your webhook, the HTTP request will contain the custom headers that you configure. These custom headers can be used to satisfy custom security schemes or other types of custom workflows for your server.

Static IP Addresses

Samsara will post to your webhook URL using static IP addresses. If your network settings restrict access for inbound connections, you can review the list of static IP addresses in your Samsara dashboard by navigating to Settings > Webhooks.

🚧

This list is subject to change, although changes are infrequent. Sign up for Webhook Changelog updates under Settings > Subscriptions for proactive notifications.

Our Webhook Signatures feature offers you a way to meet your network security requirements. See Webhook Signatures for more details.

Verify Webhook Configuration

The webhook will be listed in the webhooks table along with a secret key and a list of configured alerts that will be sent to the webhook. Note that the "Configured Alerts" column will be empty until you configure an alert to send to this webhook.

Webhook with a configured alert.

Webhook with a configured alert.

Ping Events

Ping events are sent when you click the "Test" button on the webhooks configuration page. They have an eventType of Ping, and their event object always contains: "text": "Ping".

{
  "eventId": "8928d556-72f5-439c-a924-953022c44bfb",
  "eventMs": 1589474494538,
  "eventType": "Ping",
  "event": {
    "text": "Ping"
  }
}

πŸ“˜

You can use the "Test" button to send a test "Ping" to your webhook.

2566

Send a "Ping" to your webhook using the "Test" button.

Payload Headers

HeaderDescription
X-Samsara-TimestampThe Unix epoch timestamp in seconds of when the webhook notification was sent by the Samsara server. This value is also used to create the webhook signature.
X-Samsara-SignatureA signature created using your webhook's secret key. This allows you to verify the notification is coming from Samsara. See Webhook Signatures for more details.
X-Samsara-RequestThe ID of the webhook notification. This can be helpful in debugging the issue when contacting support.
X-Samsara-Org-IdThe ID of your organization. This can be helpful in debugging the issue when contacting support.
X-Samsara-Event-TypeThe type of webhook notification. See the Payload Body section for more details.
User-AgentValue is always Samsara-Webhook/ followed by an opaque string. This should not be used to verify the request is coming from Samsara, as it can be easily spoofed. Instead, use the signature which is signed with your secret key.
Content-TypeValue is always application/json.

Webhook Signatures

Samsara provides a signature as part of the webhook HTTP request so that you can verify the request is coming from Samsara. This signature is part of the X-Samsara-Signature HTTP header.

The signature uses the Secret Key provided on the webhook configuration page.

The X-Samsara-Signature header includes a v1= prefix followed by the signature value. The signature is computed with the HMAC SHA-256 algorithm using your secret key and the message: v1:<timestamp>:<body> where <timestamp> is the X-Samsara-Timestamp HTTP header value and <body> is the request body of the webhook notification.

v1 is the only current signature version. Any other prefix to the signature indicates the request is not coming from Samsara.

Here's how you can verify the HTTP request is coming from Samsara. You can find a full code example after the step-by-step tutorial.

Step 1: Decode the secret key

The secret key displayed on the webhook configuration page is Base64 encoded. It must be decoded before use with the HMAC SHA-256 algorithm.

In this example, the secret key is: rGoy+beNph0qGBLj6Aqoydj6SGA=

import base64

secret = base64.b64decode(bytes('rGoy+beNph0qGBLj6Aqoydj6SGA=', 'utf-8'))
const secret = new Buffer.from('rGoy+beNph0qGBLj6Aqoydj6SGA=', 'base64');

Step 2: Extract the timestamp and signature from the headers

The X-Samsara-Timestamp HTTP header contains the timestamp used to generate the signature.

The X-Samsara-Signature HTTP header contains the signature provided by Samsara.

from Flask import flask, request
app = Flask(__name__)

# Using Flask
@app.route('/webhook-url', methods=['POST'])
def webhook():
    try:
        timestamp = request.headers['X-Samsara-Timestamp']
        signature = request.headers['X-Samsara-Signature']
    except KeyError:
        # missing expected headers
        return ('', 400)
    # Continue processing
const app = require('express')();
const bodyParser = require('body-parser');

// Using Express
app.post('/webhook-url', bodyParser.raw({type: 'application/json'}), (request, response) => {
  
    if (!request.header('X-Samsara-Timestamp') || !request.header('X-Samsara-Signature')) {
        console.log(request.headers);
        return response.status(400).end();
    }
    
    let timestamp = request.header('X-Samsara-Timestamp');
    let signature = request.header('X-Samsara-Signature');
    // Continue processing
});

Step 3: Prepare the message for the HMAC SHA-256 algorithm

The message is used by the HMAC SHA-256 algorithm to generate the signature. It takes the form: v1:timestamp:body.

🚧

When concatenating the request body into the HMAC message, it should be concatenated exactly as is it returned in the request. You should not post-process the body to decode any special characters. Doing so will result in an incorrect signature.

prefix = bytes('v1:' + timestamp + ':', 'utf-8')
message = prefix + request.data
let message = `v1:${timestamp}:${request.body}`;

Step 4: Determine the expected signature

Use the HMAC SHA-256 algorithm to compute the expected signature, using the secret key and the prepared message.

The expected signature will consist of the v1= prefix and the output of the HMAC algorithm in hexadecimal format.

import hmac
from hashlib import sha256

h = hmac.new(secret, message, sha256)
expected_signature = 'v1=' + h.hexdigest()
const crypto = require('crypto');

let hmac = crypto.createHmac('sha256', secret).update(message);
let expectedSignature = 'v1=' + hmac.digest('hex');

Step 5: Compare the signatures

If the expected signature does not match the signature of the request, then the request did not come from Samsara and should be ignored.

if expected_signature != signature:
    return ('', 400)
else:
    # continue processing
if (expectedSignature != signature) {
    return response.status(400).end();
} else {
    // continue processing
}

Sample Code

See above for a detailed step-by-step explanation.

In this example, the webhook's secret key is rGoy+beNph0qGBLj6Aqoydj6SGA=

from flask import Flask, request
import hmac
from hashlib import sha256
import base64

app = Flask(__name__)

# Decode the secret key from Base64 encoding
secret = base64.b64decode(bytes('rGoy+beNph0qGBLj6Aqoydj6SGA=', 'utf-8'))

# Using Flask
@app.route('/webhook-url', methods=['POST'])
def webhook():
    # Extract timestamp and signature
    try:
        timestamp = request.headers['X-Samsara-Timestamp']
        signature = request.headers['X-Samsara-Signature']
    except KeyError:
        # missing expected headers
        return ('', 400)

    # Prepare the message to sign
    prefix = 'v1:{0}:'.format(timestamp)
    message = bytes(prefix, 'utf-8') + request.data
    # Determine the expected signature
    h = hmac.new(secret, message, sha256)
    expected_signature = 'v1=' + h.hexdigest()
    # Compare signatures
    if expected_signature != signature:
        return ('', 400)
    
    # Continue processing
    
    return ('', 200)
const app = require('express')();
const bodyParser = require('body-parser');
const crypto = require('crypto');

// Decode the secret key from Base64 encoding
const secret = Buffer.from('rGoy+beNph0qGBLj6Aqoydj6SGA=', 'base64');

app.post('/webhook-url', bodyParser.raw({type: 'application/json'}), (request, response) => {
  
    if (!request.header('X-Samsara-Timestamp') || !request.header('X-Samsara-Signature')) {
        console.log(request.headers);
        return response.status(400).end();
    }
    
    let timestamp = request.header('X-Samsara-Timestamp');
    let signature = request.header('X-Samsara-Signature');

    // Prepare the message to sign
    let message = `v1:${timestamp}:${request.body}`;
    // Determine the expected signature
    let hmac = crypto.createHmac('sha256', secret).update(message);
    let expectedSignature = 'v1=' + hmac.digest('hex');
    // Compare signatures
    if (expectedSignature != signature) {
        return response.status(400).end();
    }

    // Continue processing

    response.json({received: true});
});

app.listen(port, () => console.log(`Running on port ${port}`));

What’s Next