EasyRoutes API

Webhooks Guide

Register webhook endpoints, verify signatures, and receive EasyRoutes events.

EasyRoutes webhooks allow you to receive events programmatically in response to changes in your routes, stops, drivers, and imported stops. 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:

  • An active EasyRoutes paid plan and API credentials (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

GoalSubscribe to
Update your system when a delivery completesSTOP_STATUS_UPDATED
Sync proof of delivery photos to your systemSTOP_UPDATED
Know when a driver starts their routeROUTE_STARTED
Track when routes are dispatchedROUTE_DISPATCHED
Sync imported stops back to your source systemIMPORTED_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, which interface directly with our Webhooks API. Check out some common Zapier Integrations.

Register a webhook endpoint

You can register webhook endpoints in EasyRoutes Settings or through the API.

Register in Settings

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.

Register with the API

After authenticating, send a POST request to /webhooks with your public HTTPS URL and the topics to receive:

curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourserver.com/webhooks/easyroutes",
    "topics": ["STOP_STATUS_UPDATED", "ROUTE_DISPATCHED"]
  }' \
  https://easyroutes.roundtrip.ai/api/2024-07/webhooks

The response contains the subscription ID, URL, selected topics, and whether the subscription is active. Use GET /webhooks to list subscriptions and DELETE /webhooks/{id} to remove one. Your webhook secret remains available in EasyRoutes Settings > API and is distinct from your API secret key.

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 is sent in the payload field of the body. Its type depends on the topic: Route for route and route-stop events, Driver for driver events, and ImportedStop for imported-stop events. The TEST topic has no payload.
    • 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.

FieldDescriptionExample
eventIdThe 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"
topicTopic of the webhook. See Webhook topics for a list of supported topics."ROUTE_UPDATED"
eventTimestampThe timestamp of the event that triggered the webhook."2024-08-23T16:12:26-04:00"
apiVersionThe 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. For route-stop events, this is the route-stop ID even though payload is the containing Route."rst-6ffc8a79-e556-4ca4-9246-33850da3cedc"
userUser 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 who made the update. 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. It is a Route for route and route-stop topics, a Driver for driver topics, and an ImportedStop for imported-stop topics. The TEST topic has no payload.{"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": "rst-abc123",
  "payload": {
    "id": "rte-6ffc8a79-e556-4ca4-9246-33850da3cedc",
    "name": "Monday AM Route",
    "stops": [...]
  }
}

Additionally, 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 implemented in Go:

package main

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

// Webhook secret key example.
// Not recommended 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)
}

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

TopicDescriptionPayload type
TESTTest topic generated when testing webhooks from settings.none
ROUTE_CREATEDNew route created.Route
ROUTE_UPDATEDRoute updated by planner (i.e. stop order changed, start time, etc)Route
ROUTE_DELETEDRoute deleted.Route (deleted)
ROUTE_DISPATCHEDRoute dispatched to a specific driver or available to claim.Route
ROUTE_STARTEDRoute started by driver.Route
ROUTE_COMPLETEDRoute 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_UPDATEDThe 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_UPDATEDThe end stop on the route was updated. This may correspond to the driver completing tasks or entering input on the end task.Route
STOP_UPDATEDAn 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_UPDATEDAn individual route stop status has changed. Individual stops trigger this event 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_ADDEDDriver 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_UPDATEDDriver profile updated.Driver
DRIVER_ACTIVATEDDriver activated.Driver
DRIVER_DEACTIVATEDDriver deactivated.Driver
DRIVER_ARCHIVEDDriver is archived.Driver (archived)
IMPORTED_STOP_CREATEDA new stop was imported via API.ImportedStop
IMPORTED_STOP_UPDATEDA stop imported via API was updated.ImportedStop
IMPORTED_STOP_DELETEDA 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 events 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've 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.

On this page