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:

  • 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. none
Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.