Integrate EasyRoutes Webhooks with Zapier (Advanced)
Note: The below is an advanced tutorial to connect our webhooks directly to Zapier if you need something beyond the route updated or stop status updated triggers.
Looking to use Zapier to connect with your order data? You can Import stops into EasyRoutes for planning with our Import Stop action on Zapier.
Looking to connect Zapier with EasyRoutes Webhooks? 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.
Also, take a look at some common Zapier Integrations here.
Step 1: Create a Zap Triggered by EasyRoutes Webhooks
Note that this guide assumes you have an account and basic familiarity with Zapier. A paid subscription or trial may be required to create integrations.

Sign into Zapier with your account and select + Create -> Zap from the left navigation menu to create a new "Zap" triggered by EasyRoutes webhook events.
Start by clicking on the Trigger button

Select the Webhooks By Zapier tool

For the Trigger event, select Catch Hook

Click Continue to move on to the Configure panel. Here you can choose to grab only a specific field from the webhook body, otherwise Zapier will pass on the entire payload to the connect action. Skip this screen for now to get the full webhook payload.
The Test panel will reveal the custom URL generated by Zapier for this Zap. Click on Copy so that we can use this URL to set up the webhook in EasyRoutes.

Step 2: Register a Webhook in EasyRoutes
In another tab, open EasyRoutes and head to Settings -> API. Click on Register webhook.

Paste the webhook URL from Zapier into the URL field and select the topics you want to receive.

Step 3: Test Your Webhook
After you save you webhook configuration, you can send a test event to ensure it is set up correctly with Zapier.

In everything is set up correctly, you should be able to see the TEST event record back in Zapier on the test panel for the webhook trigger. You may have to click Find new records or refresh to see the event.

Once you have generated a real webhook event (i.e. by creating a route for ROUTE_CREATED), you can view that record in Zapier to see the full set of available fields.

From there, click Continue with selected record to make these body fields available for integrations.
Example: Populate a Spreadsheet with Route Events
Save time on your Zapier setup and start with pre-filled EasyRoutes + Google Sheets Zapier templates:
Zapier allows you to select from numerous apps and tools to connect an action to the webhooks trigger. Here is an example that writes route events into a spreadsheet. Each row contains the webhook event ID, the route ID, the event timestamp, the route name, and the topic.

The fields on the Configure panel are based on the headers in the spreadsheet. You can select values based on the fields from the previously-selected webhook record above.

The result:

Example: Add Scheduled Routes to Google Calendar
Save time on your Zapier setup and start with pre-filled EasyRoutes + Google Calendar Zapier templates:
Here is another example that adds scheduled routes into a user's Google Calendar.
After configuring your Catch Hook step as described above, add a Filter by Zapier step next. Since routes require a scheduled start time in order to add them to a calendar, we'll configure this step to check that the received webhook contains a scheduled start time (Payload Scheduled For variable > Exists):

Next, we'll add the Google Calendar Find Event step (you'll need to sign in to the Google account the desired calendar is associated with), and use the route's unique identification code from the webhook (Payload ID) to see if this route has already been added as a calendar event:

If the route event is not found, this step is configured to add it to the calendar as a new event. We've used Payload Name (the route's name) for the Summary field, and Payload Scheduled For and Payload End Planned Arrival for the Start/End Dates & Times; we've also added the orders included on the route and the Route's ID to the event's description for quick reference (these are optional, and can be customized to add/remove any variables desired):

If the route event is found in the calendar, this workflow will move onto the next step - we'll configure Google Calendar's Update Event action to update the existing event we found to contain the route's updated scheduling details.
Ensure the Event field for updating is using the ID variable we found in step 3; again, use Payload Name (the route's name) for the Summary field, and Payload Scheduled For and Payload End Planned Arrival for the Start/End Dates & Times; we've also added the orders included on the route and the Route's ID to the event's description for quick reference (these are optional, and can be customized to add/remove any variables desired):

Now, whenever a new route containing a scheduled start date/time is created, or an existing route's scheduled start date/time is updated, a corresponding event will be added to (or updated in) the user's Google Calendar:

Example: Send an email when a stop is marked as complete
Save time on your Zapier setup and start with pre-filled EasyRoutes + email Zapier templates:
This example workflow will send an email to a specified email address as soon as a stop is marked as complete.
After configuring your Catch Hook step as described above, add a Filter by Zapier step next. We'll configure this step to check that the received webhook contains a successful delivery event (Payload Stops Delivery Status variable > exactly matches > DELIVERED):

Next, we'll add the Gmail Send Message step - input desired to/from addresses first, then configure the email subject to contain "Order [Payload Stops Order Name] Delivered"; the body will contain "Order [Payload Stops Order Name] has been marked as [Payload Stops Delivery Status]":

EasyRoutes → Google Sheets Daily Log: Setup Guide
This workflow captures completed delivery routes from EasyRoutes and logs each stop as a row in a Google Sheet, automatically organized into separate tabs by date.
What you'll build
When a route is marked complete in EasyRoutes, a webhook fires to Zapier. Zapier parses the route data, splits it into one row per stop, and appends each row to an Inbox tab in your Google Sheet. A script inside the spreadsheet then moves those rows to date-stamped tabs (e.g., 2026-06-26 ), creating new tabs as new dates arrive.
End result: one Google Sheet, one tab per delivery day, every stop logged with route context, customer info, delivery status, and links to photos/signatures.
Part 1: Google Sheet setup
- Create a new Google Sheet. Name it something like "EasyRoutes Daily Log."
- Rename the default tab to
Inbox. This is the staging area where new rows land before being sorted. - Add a header row in
Inbox. Row 1 should list the columns you want logged, in order. Column A must be the timestamp column — the script uses it to figure out which date's tab each row belongs to. A reasonable starting set:eventTimestamp | routeName | stopNumber | orderName | customerFullName | fullAddress | deliveryStatus | attemptedReason | itemNames | itemQuantities | deliveryPhoto | deliverySignature | deliveryNote | driverFirstName | driverLastName - Format column A as plain text. Select column A → Format → Number → Plain text. This prevents Google Sheets from auto-converting ISO timestamps into internal date serials, which causes parsing issues downstream.
Part 2: Zapier setup
Step 1: Trigger — Catch Raw Hook
- Create a new Zap. For the trigger, choose Webhooks by Zapier → Catch Raw Hook (not "Catch Hook" — the raw version is essential because it preserves the JSON structure).
- Zapier will give you a unique webhook URL. Copy it.
- In EasyRoutes, set up a webhook for the
ROUTE_COMPLETEDevent pointing at that URL. - Complete a test route (or replay one) so Zapier captures a sample payload.
Step 2: Code by Zapier — parse the webhook
EasyRoutes sends one webhook per completed route, but each route contains multiple stops. This step splits the route into individual stop records and flattens all the nested data into a flat structure that's easy to map into spreadsheet columns.
- Add an action step: Code by Zapier → Run JavaScript.
- Under Input Data, add a field keyed
rawBody, mapped to the Raw Body from the trigger. - Paste in the parsing script:
// Parse the raw webhook body
let payload;
try {
payload = typeof inputData.rawBody === 'string'
? JSON.parse(inputData.rawBody)
: inputData.rawBody;
} catch (e) {
throw new Error('Could not parse webhook body as JSON: ' + e.message);
}
const route = payload.payload || {};
const stops = route.stops || [];
const driver = route.driver || {};
// Helper to pull a value out of the EasyRoutes attributes array
const getAttr = (attributes, key) => {
if (!Array.isArray(attributes)) return '';
const found = attributes.find(a => a.key === key);
return found ? found.value : '';
};
// Build one flat object per stop
const output = stops.map(stop => {
const items = stop.items || [];
const address = stop.address || {};
const contact = stop.contact || {};
const attrs = stop.attributes || [];
// Flatten items so multi-item stops still produce a single row
const itemNames = items.map(i => i.name).join(' | ');
const itemSkus = items.map(i => i.sku).join(' | ');
const itemQuantities = items.map(i => i.quantity).join(' | ');
const totalItemQty = items.reduce((s, i) => s + (i.quantity || 0), 0);
const totalItemGrams = items.reduce((s, i) => s + (i.grams || 0) * (i.quantity || 0), 0);
const fmtAddr = a => a
? `${a.address1 || ''}, ${a.city || ''}, ${a.provinceCode || ''} ${a.zip || ''}, ${a.countryCode || ''}`
: '';
return {
// --- Event / route context (repeated on every row) ---
eventId: payload.eventId || '',
eventTimestamp: payload.eventTimestamp || '',
shopifyShop: payload.shopifyShop || '',
routeId: route.id || '',
routeName: route.name || '',
routeStatus: (route.end && route.end.status) || '',
routeCompletedAt: (route.end && route.end.updatedArrival) || '',
routeStartAddress: fmtAddr(route.start && route.start.address),
routeEndAddress: fmtAddr(route.end && route.end.address),
routeTotalDistanceM: route.totalDistanceMeters || 0,
// --- Driver ---
driverId: driver.id || '',
driverFirstName: driver.firstName || '',
driverLastName: driver.lastName || '',
driverPhone: driver.phone || '',
// --- Stop ---
stopId: stop.id || '',
stopNumber: getAttr(attrs, 'EasyRoutes Stop Number'),
stopType: stop.type || '',
deliveryStatus: stop.deliveryStatus || '',
attemptedReason: stop.attemptedReason || '',
orderName: stop.orderName || '',
shopifyOrderId: stop.shopifyOrderId || '',
updatedArrival: stop.updatedArrival || '',
distanceMeters: stop.distanceMeters || 0,
// --- Customer / address ---
customerFirstName: contact.firstName || '',
customerLastName: contact.lastName || '',
customerFullName: `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
address1: address.address1 || '',
city: address.city || '',
provinceCode: address.provinceCode || '',
zip: address.zip || '',
countryCode: address.countryCode || '',
fullAddress: fmtAddr(address),
latitude: (stop.plannedCoordinates && stop.plannedCoordinates.latitude) || '',
longitude: (stop.plannedCoordinates && stop.plannedCoordinates.longitude) || '',
// --- Items (flattened) ---
itemNames,
itemSkus,
itemQuantities,
totalItemQuantity: totalItemQty,
totalItemGrams,
// --- EasyRoutes attributes ---
easyRoutesRoute: getAttr(attrs, 'EasyRoutes Route'),
deliveryPhoto: getAttr(attrs, 'EasyRoutes Delivery Photo'),
deliverySignature: getAttr(attrs, 'EasyRoutes Delivery Signature'),
deliveryNote: getAttr(attrs, 'EasyRoutes Delivery Note'),
signatureDisclosure: stop.customSignatureDisclosure || ''
};
});
return output;
- The script also computes a
worksheetNamefield, though for this Apps Script setup we don't strictly need it — the spreadsheet handles tab routing itself.
Why this works: when a Code step returns an array, Zapier automatically runs the rest of the Zap once per array item. So a route with three stops produces three runs of the next step.
Step 3: Action — Google Sheets: Create Spreadsheet Row
- Add a Google Sheets → Create Spreadsheet Row action.
- Connect your Google account, pick the spreadsheet, and choose the
Inboxworksheet. - Map each column to the corresponding field from the Code step's output. Make sure column A maps to
eventTimestamp(or whichever timestamp field you chose). - Test the step. You should see three rows land in
Inbox.
That's the entire Zapier side. From here, the spreadsheet takes over.
Part 3: Apps Script setup (the sorting logic)
This script watches the Inbox tab, and whenever new rows appear, it moves them to date-stamped tabs based on column A's timestamp. New tabs are created automatically as new dates show up.
Install the script
- In your Google Sheet, open Extensions → Apps Script.
- Delete any starter code and paste in the script:
const INBOX_SHEET_NAME = 'Inbox';
const TIMEZONE = 'America/Toronto';
const DATE_FORMAT = 'yyyy-MM-dd'; // tab name format
const TIMESTAMP_COLUMN = 1; // column A holds the stop's timestamp
function onChange(e) {
// Only react to row inserts/edits, not formatting changes etc.
if (e && e.changeType && !['INSERT_ROW', 'EDIT', 'PASTE', 'OTHER'].includes(e.changeType)) {
return;
}
processInbox();
}
function processInbox() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const inbox = ss.getSheetByName(INBOX_SHEET_NAME);
if (!inbox) throw new Error(`Sheet "${INBOX_SHEET_NAME}" not found`);
const lastRow = inbox.getLastRow();
const lastCol = inbox.getLastColumn();
if (lastRow < 2) return; // header only, nothing to move
// Read everything except the header row
const data = inbox.getRange(2, 1, lastRow - 1, lastCol).getValues();
const headers = inbox.getRange(1, 1, 1, lastCol).getValues()[0];
// Group rows by destination tab name (date string)
const buckets = {};
data.forEach(row => {
const ts = row[TIMESTAMP_COLUMN - 1];
if (!ts) return; // skip blank rows
const date = (ts instanceof Date) ? ts : new Date(ts);
if (isNaN(date)) return;
const tabName = Utilities.formatDate(date, TIMEZONE, DATE_FORMAT);
if (!buckets[tabName]) buckets[tabName] = [];
buckets[tabName].push(row);
});
// Write each bucket to its destination tab, creating + adding headers if needed
Object.keys(buckets).forEach(tabName => {
let sheet = ss.getSheetByName(tabName);
if (!sheet) {
sheet = ss.insertSheet(tabName);
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.setFrozenRows(1);
}
const rows = buckets[tabName];
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
.setValues(rows);
});
// Clear the inbox (everything except the header)
inbox.getRange(2, 1, lastRow - 1, lastCol).clearContent();
}
- Adjust the constants at the top if needed:
INBOX_SHEET_NAME— should match your inbox tab name.TIMEZONE— set to your local timezone so the "day" boundary falls correctly.DATE_FORMAT—yyyy-MM-ddproduces tabs like2026-06-26.
- Save the project (give it any name).
Add the trigger
- In the Apps Script editor, click the clock icon (Triggers) in the left sidebar.
- Click + Add Trigger.
- Configure: function
onChange, event sourceFrom spreadsheet, event typeOn change. - Save. Google will prompt you to authorize the script — accept the permissions.
How the script behaves
Each time Zapier writes new rows to Inbox , the trigger fires and the script:
- Reads everything below the header row.
- Groups rows by the date in column A.
- For each date, makes sure a tab with that name exists (creating it with the same headers if not).
- Appends the rows to the correct tab.
- Clears the inbox so it's ready for the next batch.
Part 4: Testing
- Complete a test route in EasyRoutes (or trigger a webhook replay).
- Watch
Inbox— rows should appear within a few seconds, then disappear as the script moves them. - A new tab named with today's date should appear (or be appended to if it already exists).
- Try a route on a different day (or manually edit a row's timestamp) to confirm new tabs are created correctly.
Things to know and watch for
- Use Catch Raw Hook, not Catch Hook. The non-raw version pre-flattens the JSON and breaks the parsing approach entirely.
- Only the first bundle is visible in Zapier's test view. When the Code step returns three stops, Zapier shows "1 of 3" in the test output — page through to confirm all three are there. The live Zap will process all of them.
- The
Inboxheader row is the source of truth. All daily tabs inherit their column order fromInbox, so set it up carefully the first time. Changing it later means existing daily tabs won't match. - Column A must be formatted as plain text to prevent Google Sheets from converting ISO timestamps into internal serial numbers (which would break the script's date parsing).
On changetriggers are reliable but not bulletproof. If you find rows occasionally getting stuck inInbox, switch to a time-based trigger (every 1 or 5 minutes) running the same function — it polls instead of reacting, which is more robust.- The script clears
Inboxafter each run. If you want a safety net, modify it to archive rows to a separate tab instead of deleting them. - Timezone matters for the day boundary. A delivery completed at 11:30 PM local time is already past midnight UTC — make sure the script's
TIMEZONEconstant matches where you want days to break. - Headers in daily tabs are copied from
Inboxat creation time. If you change theInboxheaders later, existing daily tabs will keep their old structure.