EasyRoutes API - Webhooks Guide

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 up to 10 times with exponential back-off for up to ~5 hours but are 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.