Integration API
Connect any order system.
Send Porch Route the deliveries you need made, keyed by your own order reference. It plans them into routes, your drivers work them, and each outcome comes back to you over a signed webhook carrying the same reference — so whatever your system does for a delivered order, it keeps doing.
Base url https://porchroutes.com/api/v1 · JSON in, JSON out · every success is { data, message }
Machine-readable: OpenAPI 3.1 · this page as Markdown
Last updated By Porch Route
Authentication
Create an API key in Integrations → API keys. The full key is shown once; Porch Route stores only a hash of it. Send it as a bearer token on every request. A key belongs to one business and only ever reaches that business’s deliveries.
Every write needs an Idempotency-Key header — any unique string per logical change, such as your order number plus a version. Retrying with the same key returns the original response instead of making the change twice.
Authorization: Bearer pr_live_… Idempotency-Key: order-10241-v1 Content-Type: application/json
Create or update a delivery
POST/api/v1/orders
Creates the delivery, or updates it if one with the same external_ref already exists. Call it when an order is confirmed for delivery and again whenever it changes.
{
"external_ref": "10241",
"customer_name": "Priya Raman",
"customer_phone": "+14355550110",
"customer_email": "priya@example.com",
"address": {
"line1": "312 W Tabernacle St",
"city": "St. George",
"region": "UT",
"postal_code": "84770",
"lat": 37.1078,
"lng": -113.5871
},
"instructions": "Cooler on the side porch, gate code 4417",
"delivery_date": "09/17/2026",
"window_start": "09:00",
"window_end": "12:00",
"temperature_class": "FROZEN",
"load_weight_lbs": 42,
"items": [
{ "name": "Ribeye steak", "quantity": 4, "unit_weight_lbs": 0.75, "temperature_class": "FROZEN" }
]
}external_refrequired- Your reference. Stored as an opaque string and returned on every webhook — never parsed, so “00123” stays “00123”.
customer_namerequired- Shown to the driver. Phone and email are optional and are how the customer gets their tracking link.
addressrequiredline1is required. Sendlatandlngtogether to skip geocoding; otherwise the address is geocoded on arrival, and a stop that can’t be placed is flagged for the office instead of being dropped.delivery_daterequiredMM/dd/yyyyin your business’s time zone.window_startandwindow_endare optionalHH:mmtimes in the same zone.temperature_classDRY,REFRIGERATEDorFROZEN— decides which vehicles may carry the stop. Defaults to the coldest class amongitems, elseDRY.load_weight_lbs- Omit or send
nullwhen you don’t know it. Unknown is never treated as zero; when every item has aunit_weight_lbs, the total is worked out for you. items- Line items for the driver’s manifest:
name,quantity, and optionallyunit_weight_lbs,temperature_class,sku. metadata- Any JSON object you want kept with the delivery. Porch Route doesn’t read it.
Once a delivery is on a dispatched route, only items and instructions can change; any other change returns 409 with the reason, because the driver is already working from that plan. Delivered, failed and cancelled deliveries can’t be changed — to deliver a cancelled order again, send a new external_ref.
Read a delivery
GET/api/v1/orders/{external_ref}
Current status, where it sits on a route, the stop’s outcome and the customer’s tracking link.
{
"data": {
"order_uuid": "8f0c…",
"external_ref": "10241",
"status": "DELIVERED",
"delivery_date": "09/17/2026",
"temperature_class": "FROZEN",
"route": { "route_uuid": "b21e…", "name": "North loop", "status": "COMPLETED" },
"stop": {
"stop_uuid": "5d7a…",
"status": "DELIVERED",
"delivered_at": "2026-09-17T15:42:11Z",
"tracking_url": "https://porchroutes.com/track/…"
}
},
"message": "OK"
}status is one of OPEN (waiting to be routed), SCHEDULED (on a route), DELIVERED, FAILED or CANCELLED.
Cancel a delivery
POST/api/v1/orders/{external_ref}/cancel
Takes the delivery off its route (if it’s on one) and marks it CANCELLED. Refused with 409 once the stop is out for delivery — a driver is already on the way — and for deliveries already delivered or failed. Cancelling twice is harmless. Needs an Idempotency-Key.
Webhooks
Add an endpoint in Integrations → Webhooks and choose its events (none selected means all). Porch Route posts JSON to it with three headers, and shows you the signing secret once.
X-PorchRoute-Signature: sha256=<hmac of the raw body>
X-PorchRoute-Event: stop.delivered
X-PorchRoute-Delivery: <unique id — dedupe on it>
{
"event": "stop.delivered",
"occurred_at": "2026-09-17T15:47:11Z",
"data": {
"external_ref": "10241",
"order_uuid": "8f0c…",
"stop_uuid": "5d7a…",
"route_uuid": "b21e…",
"route_date": "09/17/2026",
"status": "DELIVERED",
"occurred_at": "2026-09-17T15:42:11Z",
"delivered_at": "2026-09-17T15:42:11Z",
"delivered_on": "09/17/2026",
"arrived_at": "2026-09-17T15:39:02Z",
"reason": null,
"driver_notes": "Left in the porch cooler",
"driver": { "name": "Marisol Vega" },
"proof": {
"photo_urls": ["https://…"],
"signature_url": null,
"urls_expire_at": "2026-09-24T15:47:11Z"
},
"tracking_url": "https://porchroutes.com/track/…",
"undo_count": 0
}
}stop.scheduledThe delivery was put on a route.stop.en_routeThe driver is heading to this stop next.stop.deliveredDelivered. Sent after your undo window, so a mis-tap undone in time never reaches you.stop.failedThe driver couldn't deliver.reasonsays why.stop.skippedSkipped for now; the delivery goes back to OPEN to be routed again.stop.undoneAn outcome you already received was reversed.undone_statussays which.order.unassignedA scheduled delivery was taken off its dispatched route and is back to OPEN.route.completedThe driver finished the route.
delivered_on is the date in your business’s time zone. Proof urls are signed and expire at urls_expire_at — copy the files if you need to keep them.
Verifying a signature
The signature is an HMAC-SHA256 of the raw body, keyed on the endpoint’s signing secret. Verify before parsing: re-serialising JSON changes key order and whitespace, and the signature won’t match.
import { createHmac, timingSafeEqual } from "crypto";
export function verify(rawBody, header, secret) {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
if (!header || header.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Your API key and your signing secret do different jobs. The key identifies you on requests you send; the secret proves a webhook came from Porch Route and never travels in a header. Rotate the secret from the Webhooks screen whenever someone who had it leaves.
Retries and reconciliation
- Answer 2xx quickly. Anything else counts as a failed attempt. Do slow work after you respond.
- Failed deliveries retry with backoff — immediately, then after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours. A 4xx other than 408 or 429 means your endpoint said no, so it isn't retried.
- Dedupe on X-PorchRoute-Delivery. A retry carries the same id, so handling an event twice is always safe to skip.
- Nothing is silently lost. A reconciliation sweep reports outcomes you're still owed, and the Integrations screen lets you retry any delivery by hand.
Errors
Errors return a status code and a sentence you can show a person. Validation errors add details with the field that failed.
{
"statusCode": 409,
"message": "This order is on a route that has already gone out, so only items and instructions can change. Ask the dispatcher to take it off the route first.",
"details": { "fields": ["delivery_date"] }
}400 invalid body · 401 missing or revoked key · 404 no delivery with that reference · 409 the change conflicts with the delivery’s state · 429 slow down and retry.
Health
GET/api/v1/health
No key needed. Returns 200 while the API and its database are up (503 when the database can’t be reached), with a checks block for the database, geocoder, map tiles and the jobs tick — the same data as the status page. Point your monitor at it.