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
POSTwebhook 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
RouteorRouteStop) is sent in thepayloadfield 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 For EasyRoutes for web users, this corresponds to the For driver-initiated actions, the All kinds of webhooks may also include an |
EasyRoutes for Shopify update:
EasyRoutes for Web update:
Driver update:
Example with IP address and user agent:
|
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-IdX-EasyRoutes-TopicX-EasyRoutes-Event-TimestampX-EasyRoutes-VersionX-EasyRoutes-Shopify-ShopX-EasyRoutes-OrganizationX-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:
The entire |
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 This will trigger for all stops when the route is marked as ready. Note: |
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.
