EasyRoutes API - Webhooks Guide

EasyRoutes API is currently in public preview. During the public preview phase, frameworks may be subject to potential changes.

EasyRoutes webhooks support allows you to receive events programmatically in response to changes in your routes. In combination with using the EasyRoutes API, webhook events allow you to build powerful automation triggered by deliveries, route updates, or other events.

What you'll need:

  • API access enabled (see Getting Started Guide)
  • A server with an HTTPS endpoint that can receive POST requests
  • Familiarity with HMAC signature verification (optional but recommended)

Common use cases

Goal Subscribe to
Update your system when a delivery completes STOP_STATUS_UPDATED  
Sync proof of delivery photos to your system STOP_UPDATED  
Know when a driver starts their route ROUTE_STARTED  
Track when routes are dispatched ROUTE_DISPATCHED  
Sync imported stops back to your source system IMPORTED_STOP_CREATED  , IMPORTED_STOP_UPDATED  

Prefer no-code? Set up Zaps based on route- and stop-level updates using our Route Updated & Stop Status Updated Zapier triggers that interfaces directly with our Webhooks API instead. Check out some common Zapier Integrations.

1. Register a webhook endpoint

Go to EasyRoutes Settings > API and click Register webhook.

Configure:

  • URL: Your HTTPS endpoint (e.g., https://yourserver.com/webhooks/easyroutes  )
  • API version: Determines the payload format
  • Topics: Which events to receive (see Webhook topics below)

Creating your first webhook generates a Webhook secret — you'll use this to verify requests are from EasyRoutes. See Verifying webhook requests for details on how to secure your endpoint.

2. Build a webhook handler

To receive webhooks, you need to:

  • Accept POST   webhook requests on an HTTPS endpoint or server
    • Relevant webhook metadata like the topic and affected object ID are sent via headers as well as in the JSON body of the request.
    • The affected object itself (i.e. a Route   or RouteStop   ) is sent in the payload   field of the body.
    • See Webhook event overview below for details on the request format.
  • Return a successful status code (2xx   ) to acknowledge the webhook quickly (within 10 seconds).
    • Avoid doing any complex logic that could cause a timeout synchronously in your endpoint handler.
  • (Recommended) Verify the HMAC signature — see below.

Webhook event overview

All EasyRoutes webhook requests share a set of common fields, regardless of API version. These fields are serialized in a JSON payload in the body of the POST   request.

Field Description Example
eventId    The unique ID of the webhook event. In rare cases of duplicate webhook delivery, this can be used to deduplicate work. "evt-fa75b82d-2411-4cf8-b15f-cf3d8c91e919"   
topic    Topic of the webhook. See Webhook topics for a list of supported topics. "ROUTE_UPDATED"   
eventTimestamp    The timestamp of the event that triggered the webhook. "2024-08-23T16:12:26-04:00"  
apiVersion    The API version of the payload object. "V2024_07"   
shopifyShop   (optional) The Shopify shop connected to EasyRoutes (Shopify only) "easyroutes.myshopify.com"   
organization   (optional) The unique organization identifier for EasyRoutes for Web "roundabout-supply-co-a5c2"  
objectId   (optional) The ID of the affected object (i.e. Route   ID). "rte-6ffc8a79-e556-4ca4-9246-33850da3cedc"   
user   

User that triggered the event. The field is structured and different fields are populated for different update scenarios.

For EasyRoutes in Shopify, this corresponds to the shopifyUserId   of the Shopify admin user for your store that took the action.

For EasyRoutes for web users, this corresponds to the email   of the authenticated member that made the updated.

For driver-initiated actions, the driverId   is populated with the driver's ID (matching the API format).

All kinds of webhooks may also include an ipAddress   and userAgent   of the initiating user (if available).

EasyRoutes for Shopify update:

{"shopifyUserId": "96343753011"}  

EasyRoutes for Web update:

{"email":"john.doe@roundtrip.ai"}  

Driver update:

{"driverId":"drv-cb93e344-fb1d-4d34-b8a8-8df5cdd9e939"}   

Example with IP address and user agent:

{"ipAddress": "37.19.211.58", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"}  

payload   (optional) The affected object (i.e. Route   ). {"id":"rte-...", "name":"#878", "start":{...}, ...}   

Sample:

{
  "eventId": "evt-fa75b82d-2411-4cf8-b15f-cf3d8c91e919",
  "topic": "STOP_STATUS_UPDATED",
  "eventTimestamp": "2024-08-23T16:12:26-04:00",
  "apiVersion": "V2024_07",
  "objectId": "stp-abc123",
  "payload": {
    "id": "rte-6ffc8a79-e556-4ca4-9246-33850da3cedc",
    "name": "Monday AM Route",
    "stops": [...]
  }
}

Additional some of the metadata fields are mirrored in HTTP headers for convenience.

  • X-EasyRoutes-Event-Id   
  • X-EasyRoutes-Topic   
  • X-EasyRoutes-Event-Timestamp   
  • X-EasyRoutes-Version   
  • X-EasyRoutes-Shopify-Shop   
  • X-EasyRoutes-Organization   
  • X-EasyRoutes-Object-Id   

Verifying webhook requests

EasyRoutes webhooks are signed using your unique Webhook secret   from Settings. The HMAC SHA256 hash is sent in the webhook request via the X-EasyRoutes-Hmac-Sha256   header. To verify that requests are sent by EasyRoutes, compute your own signature from the request body and verify it against the header signature.

Note: your webhook secret is distinct from your API secret key and is always available on the Settings page.

Example webhook endpoint

Here is an example endpoint implement in Go:

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/json"
  "io"
  "log"
  "net/http"
)

// Webhook secret key example.
// Not recommend to hardcode!
const easyroutesSecret = "PjV...w=="

type Event struct {
  EventId        string `json:"eventId"`
  Topic          string `json:"topic"`
  EventTimestamp string `json:"eventTimestamp"`
  ApiVersion     string `json:"apiVersion"`
  ObjectId       string `json:"objectId,omitempty"`
  Payload        []byte `json:"payload,omitempty"`
}

func main() {
  http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
    b, err := io.ReadAll(r.Body)
    if err != nil {
      log.Printf("error reading webhook body: %v", err)
      w.WriteHeader(http.StatusServiceUnavailable)
      return
    }

    sig := r.Header.Get("X-EasyRoutes-Hmac-Sha256")
    if len(sig) == 0 {
      log.Printf("missing signature header")
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    d, err := base64.StdEncoding.DecodeString(easyroutesSecret)
    if err != nil {
      log.Fatalf("error decoding secret: %v", err)
    }

    enc := hmac.New(sha256.New, d)
    enc.Write(b)
    expected := enc.Sum(nil)
    actual, err := base64.StdEncoding.DecodeString(sig)
    if err != nil {
      log.Printf("error decoding signature: %v", err)
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    if !hmac.Equal(expected, actual) {
      log.Printf(
        "signature mismatch, expected=%s, actual=%s",
        base64.StdEncoding.EncodeToString(expected),
        base64.StdEncoding.EncodeToString(actual),
      )
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    parsed := &Event{}
    err = json.Unmarshal(b, parsed)
    if err != nil {
      log.Printf("error parsing webhook body: %v", err)
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    switch parsed.Topic {
    case "ROUTE_CREATED":
      // process event here
    }

    w.WriteHeader(http.StatusOK)
  })

  http.ListenAndServe(":8080", nil)
}

4. Testing your webhook

Once you've built your webhook endpoint and registered it in EasyRoutes, you can send a test event (topic   = TEST   ) to verify everything is working as expected.

If your endpoint returns 2xx  , you're ready.


Webhook topics

Topic Description Payload type
TEST    Test topic generated when testing webhooks from settings. none
ROUTE_CREATED    New route created. Route   
ROUTE_UPDATED    Route updated by planner (i.e. stop order changed, start time, etc) Route   
ROUTE_DELETED    Route deleted. Route   (deleted)
ROUTE_DISPATCHED   Route dispatched to a specific driver or available to claim. Route  
ROUTE_STARTED   Route started by driver. Route  
ROUTE_COMPLETED   

Route completed by driver.

For a route that ends at the last stop, this is triggered when all stops are complete (attempted or delivered).

For a route with an explicit end stop, this is triggered when the route is marked complete.

Route   
ROUTE_START_STOP_UPDATED    The start stop on the route was updated. This may correspond to the driver completing tasks or entering input on the start task. Route   
ROUTE_END_STOP_UPDATED   The end stop on the route was updated. This may correspond to the driver completing tasks or entering input on the end task. Route   
STOP_UPDATED  

An individual route stop was updated. This includes:

  • Stop status updated (deliveryStatus   or breakStatus   )
  • Stop proof of delivery updated (proofOfDeliveryNote   , proofOfDeliveryPhotos   or proofOfDeliverySignatures   )
  • Stop tasks   updated (items completed or input added)

The entire Route   is returned in the payload for convenience while the objectId   field and X-EasyRoutes-Object-Id   header indicate which stop was updated.

Route   
STOP_STATUS_UPDATED   

An individual route stop status has changed. Individual stops are triggered as their status is updated during delivery (i.e. out for delivery, attempted, delivered, etc). Break stops will also trigger this event when completed.

The entire Route   is returned in the payload for convenience while the objectId   field and X-EasyRoutes-Object-Id   header indicate which stop was updated.

This will trigger for all stops when the route is marked as ready.

Note: STOP_UPDATED   includes all of the above as well, so clients should subscribe to one or the other but not both.

Route   
DRIVER_ADDED  

Driver added to shop.

Note that this event fires after an invited driver completes sign-up through the driver app. There are no events for pending drivers or invites themselves.

Driver  
DRIVER_UPDATED   Driver profile updated. Driver  
DRIVER_ACTIVATED   Driver activated. Driver  
DRIVER_DEACTIVATED   Driver deactivated. Driver  
DRIVER_ARCHIVED   Driver is archived. Driver   (archived)
IMPORTED_STOP_CREATED   A new stop was imported via API. ImportedStop   
IMPORTED_STOP_UPDATED    A stop imported via API was updated. ImportedStop   
IMPORTED_STOP_DELETED   A stop imported via API was deleted. ImportedStop   (deleted)

Guidelines and best practices

The following section outlines some guidelines and best practices to be aware of when designing your EasyRoutes webhook handler.

Respond in a timely manner

Your handler should respond to a webhook request with a 2xx response within 10 seconds. If you take longer than that to respond, EasyRoutes may consider the delivery a failure. Failed webhooks are retried with back-off but eventually dropped. Webhook endpoints that consistently fail may be deactivated (see below).

To respond quickly, avoid doing any heavy processing synchronously or making expensive downstream network calls to storage or other services. Consider setting up a queue to process webhook event asynchronously after responding immediately.

Handle duplicate events

In rare cases, EasyRoutes may send the same webhook event multiple times, even if your server acknowledged the request with a 2xx response. These retries can happen due to timeouts, dropped network traffic, or transient unavailability.

To avoid duplicate work, your webhook handler can use the eventId   / X-EasyRoutes-Event-Id   header as a unique event identifier.

Handle delayed or out-of-order events

Just as above, events may arrive out of order or delayed due to retries or transient unavailability. Your webhook handler can use the eventTimestamp   / X-EasyRoutes-Event-Timestamp   header to produce an ordering of events for a given object (i.e. a route). Using this, you may choose to ignore "earlier" events if you're already processed "later" ones for a given object.

Prevent replay attacks

You can verify that webhooks were sent by EasyRoutes by validating that the SHA256 HMAC of the request body computed using your webhook secret key matches the X-EasyRoutes-Hmac-Sha256   header on the request. However, to prevent replay or man-in-the-middle attacks you may also consider not processing webhooks (just responding with 2xx) if the eventTimestamp   in the body is too old. The body timestamp cannot be changed by an attacker without affecting the computed signature, which they could only recompute if your webhook secret key is compromised.


Deactivated webhooks

Webhook endpoints that consistently fail to acknowledge events (return 2xx response within 10 seconds) may be deemed inactive and be deactivated by our system. Deactivated webhooks are highlighted with a badge on the API settings page:

To reactivate a deactivated webhook, just Edit and Save the webhook endpoint. However, if it continues to fail, it may be deactivated again in the future.

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.