EasyRoutes Webhooks API
The EasyRoutes API is currently available in a closed beta. To request access, please contact us.
EasyRoutes webhooks support allows you to receive events programmatically in response to changes in your routes. In combination with EasyRoutes API support, webhook events allow you to build powerful automation triggered by deliveries, route updates, or other events.
Learn how to integrate EasyRoutes Webhooks with Zapier here.
Create a webhook handler
To receive webhooks, you need a server with an HTTPS endpoint that can accept POST
webhook requests. 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.
Your webhook handler should return a successful status code (2xx
) to acknowledge the webhook quickly. Avoid doing any complex logic that could cause a timeout synchronously in your endpoint handler.
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" |
payload (optional) |
The affected object (i.e. Route ). |
{"id":"rte-...", "name":"#878", "start":{...}, ...} |
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-Organiation
X-EasyRoutes-Object-Id
Register a webhook endpoint
To register your webhook endpoint, navigate to Settings
> API Settings
and click Register webhook
.
In the creation modal, you can specify the URL of the server endpoint you want to receive webhooks on. The API version determines the format of the data payloads you receive in the body of the webhook. Additionally, you must specify one or more topics that you wish to receive events for (see Webhook topics below for a description of each event and the expected payload).
Creating your first webhook will also generate a Webhook secret
that will be used to sign all requests to your endpoint. See Verifying webhook requests for details on how to secure your endpoint.
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.
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) }
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. | none |
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 |
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. | none |