# Porch Route integration API

> Send deliveries in by your own order reference; get every outcome back over a signed webhook.

https://porchroutes.com/docs/api · Last updated September 14, 2026 · OpenAPI 3.1: https://porchroutes.com/docs/openapi.json · Markdown: https://porchroutes.com/docs/api.md

Create or update deliveries keyed by your order reference, read their status, cancel them, check which delivery days a location can still be ordered for, and receive signed webhooks when each stop is scheduled, delivered, failed or undone. Build, read, dispatch and cancel the day's routes and record stop outcomes — the office's own actions — and solve routes for any depot, vehicles and stops with the Engine API. Every success is `{ data, message }`; every error is `{ statusCode, message, details? }`. Deliveries sent through this API are never rejected for falling outside the business's delivery zones: the caller's checkout already decided eligibility.

Base url: `https://porchroutes.com/api/v1` · JSON in, JSON out.

## Authentication

`Authorization: Bearer pr_live_…` — created in Integrations → API keys, shown once, stored as a hash. A key belongs to one business.

Every write needs an `Idempotency-Key` header: any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409.

```
Authorization: Bearer pr_live_…
Idempotency-Key: order-10241-v1
Content-Type: application/json
```

## Endpoints

### GET /orders — Deliveries changed since an instant

Oldest change first, a page at a time: pass `next_cursor` back as `cursor`. Each item is the same Order shape `GET /orders/{external_ref}` returns. For polling integrations and reconciliation.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `since` | query | no | Deliveries created or changed at or after this instant. |
| `status` | query | no | One status or a comma list (OPEN, SCHEDULED, DELIVERED, FAILED, CANCELLED). |
| `date` | query | no | MM/dd/yyyy delivery date. |
| `limit` | query | no |  |
| `cursor` | query | no | `next_cursor` from the previous page. |

- `200` — A page of deliveries; the body also carries `next_cursor` (null on the last page).
- `400` — Bad query or cursor.
- `401` — Missing, malformed, unknown or revoked API key.

### POST /orders — Create or update a delivery by external_ref

The body is the order as your system sees it now (a full replacement, not a patch). Sending an unchanged order is a no-op. Once the delivery is on a dispatched route only `items`, `instructions`, `metadata` and texting consent can change; other changes return 409 with the fields in `details`. Delivered, failed and cancelled deliveries can't be changed — send a new `external_ref` to deliver again.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header | yes | Any unique string per logical change (an order number plus a version works). Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

Request body: `OrderInput` (see the OpenAPI document for every field).

- `200` — Updated (or unchanged).
- `201` — Created.
- `400` — Invalid body — `message` names the first bad field, `details` lists them all.
- `401` — Missing, malformed, unknown or revoked API key.
- `409` — The delivery's state doesn't allow this change, or the idempotency key was reused with a different body.
- `429` — Slow down and retry.

### GET /orders/{external_ref} — Read a delivery

Current status, where it sits on a route, the stop's outcome and the customer's tracking link.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `external_ref` | path | yes | Your order reference, URL-encoded. |

- `200` — The delivery.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No delivery with that reference for your business.

### POST /orders/{external_ref}/cancel — Cancel a delivery

Takes the delivery off a route that hasn't started and marks it CANCELLED. Refused with 409 once the stop is out for delivery, or once it's delivered or failed. Cancelling twice is harmless.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `external_ref` | path | yes | Your order reference, URL-encoded. |
| `Idempotency-Key` | header | yes | Any unique string per logical change (an order number plus a version works). Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

- `200` — Cancelled (or already cancelled).
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No delivery with that reference for your business.
- `409` — Out for delivery, delivered or failed — too late to cancel.

### GET /availability — Which delivery days a location can still be ordered for

The active zones containing the point (most specific first), each open day with its window, remaining capacity and order cutoff, and every closed day with the reasons. `served: false` with no zones means the location is outside every zone — an answer, not an error. Advisory only: `POST /orders` never rejects an order for its zone or date. Limits per API key: 120 checks a minute, of which 30 may be `address` lookups.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `lat` | query | no | Send lat and lng together (cheapest — no geocoding), or `address`. |
| `lng` | query | no |  |
| `address` | query | no | A full address, geocoded here. |
| `from` | query | no | First day to consider (yyyy-MM-dd). Defaults to today in the business's zone; never earlier. |
| `days` | query | no |  |

- `200` — Open and closed days per zone.
- `400` — Send lat and lng, or an address.
- `401` — Missing, malformed, unknown or revoked API key.
- `422` — The address couldn't be found.
- `429` — Too many checks or lookups — see the limits.
- `503` — The geocoder is down; offer your own standard days rather than blocking checkout.

### GET /health — Liveness

No key needed. 200 while the API is up; `distance_source` is the configured road-distance provider.

No API key needed.

- `200` — Alive.

### GET /routes — The day's routes

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `date` | query | yes | MM/dd/yyyy in the business's time zone. |

- `200` — Routes on that date, without stops.
- `400` — `date` must be MM/dd/yyyy.
- `401` — Missing, malformed, unknown or revoked API key.

### POST /routes/build — Build the day's routes

The same build the office runs: temperature classes are hard, unknown weight never binds capacity, locked stops never move. Every active vehicle without a route that day is used unless `vehicle_ids` says otherwise. Up to 300 deliveries per build; 10 builds a minute per key.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

Request body: `BuildInput` (see the OpenAPI document for every field).

- `201` — Routes built; `unassigned` lists what was left off and why.
- `400` — Invalid body.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — A listed reference doesn't exist for your business.
- `409` — A reference isn't OPEN or isn't on the date, no vehicle is active, a build is already running, or nothing is routable.
- `429` — Too many builds.

### GET /routes/{route_uuid} — A route with its stops

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `route_uuid` | path | yes | The route's uuid, from `GET /routes`. |

- `200` — The route, stops in driving order.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No route with that uuid for your business.

### POST /routes/{route_uuid}/dispatch — Send the route to its driver

Exactly the office's "Send to driver": the plan is committed (`stop.scheduled` webhooks, the customers' scheduled notice when the business has it on) and the driver link is minted and texted to the assigned driver. The link is never returned.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `route_uuid` | path | yes | The route's uuid, from `GET /routes`. |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

- `200` — Dispatched.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No route with that uuid for your business.
- `409` — The route is cancelled, has no stops, or its drive window has passed.

### POST /routes/{route_uuid}/cancel — Cancel a route

Unfinished stops come off — their deliveries go back to OPEN and `order.unassigned` is sent for each when the route had been dispatched — finished stops stay, the driver link dies and `route.cancelled` goes out. Customers aren't messaged.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `route_uuid` | path | yes | The route's uuid, from `GET /routes`. |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

- `200` — Cancelled.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No route with that uuid for your business.
- `409` — A completed or already cancelled route can't be cancelled.

### POST /routes/{route_uuid}/stops/{stop_uuid}/status — Record a stop's outcome

The drive plane's rules: the route must be running; DELIVERED and FAILED are held for the business's undo window before `stop.delivered` / `stop.failed` and the customer notice go out; SKIPPED releases the delivery back to OPEN at once. A finished stop can't be changed to another outcome without an undo.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `route_uuid` | path | yes | The route's uuid, from `GET /routes`. |
| `stop_uuid` | path | yes | The stop's uuid, from `GET /routes/{route_uuid}`. |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

Request body: `StopStatusInput` (see the OpenAPI document for every field).

- `200` — Recorded.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No route with that uuid for your business.
- `409` — The route isn't running, the stop is already finished, or the business requires a photo or signature first.

### POST /optimize — Solve routes for a depot, vehicles and stops

The Engine API: stateless, nothing stored. Road distances come from the business's configured provider; past its table size (61 points) or when it's down the answer uses straight-line estimates and says so. Arrival times are HH:mm on the day being planned. 30 solves a minute per key.

Request body: `OptimizeInput` (see the OpenAPI document for every field).

- `200` — Routes, arrival times and every stop left off with its reason.
- `400` — Invalid body — duplicate ids, bad times, or over 50 vehicles / 1,000 stops.
- `401` — Missing, malformed, unknown or revoked API key.
- `429` — Too many solves.

### GET /webhooks/subscriptions — The business's webhook endpoints

Every endpoint, whoever added it — no secrets.

- `200` — Endpoints.
- `401` — Missing, malformed, unknown or revoked API key.

### POST /webhooks/subscriptions — Add a webhook endpoint

The REST-hook subscribe step for tools like Zapier. Same rules as endpoints added in Integrations: public https only, at most 10 per business. The signing secret is returned once.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

Request body: `WebhookSubscriptionInput` (see the OpenAPI document for every field).

- `201` — Added.
- `400` — Not a public https URL, or an unknown event.
- `401` — Missing, malformed, unknown or revoked API key.
- `409` — The business already has 10 endpoints.

### PATCH /webhooks/subscriptions/{endpoint_uuid} — Change or pause a webhook endpoint

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `endpoint_uuid` | path | yes |  |
| `Idempotency-Key` | header | yes | Any unique string per logical change. Retrying with the same key and body replays the original response; the same key with a different body is a 409. |

Request body: `WebhookSubscriptionUpdate` (see the OpenAPI document for every field).

- `200` — Updated.
- `400` — Not a public https URL, or an unknown event.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No endpoint with that uuid for your business.

### DELETE /webhooks/subscriptions/{endpoint_uuid} — Remove a webhook endpoint

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `endpoint_uuid` | path | yes |  |

- `200` — Removed; anything still owed to it is abandoned.
- `401` — Missing, malformed, unknown or revoked API key.
- `404` — No endpoint with that uuid for your business.

## Webhooks

Add an endpoint in Integrations → Webhooks and choose its events (none selected means all). Porch Route posts JSON with three headers — `X-PorchRoute-Signature: sha256=<hex>` (HMAC-SHA256 of the raw body, keyed on the endpoint's signing secret), `X-PorchRoute-Event` and `X-PorchRoute-Delivery` (unique per delivery; a retry carries the same id — dedupe on it). Verify the signature over the raw body before parsing.

| Event | Meaning |
| --- | --- |
| `stop.scheduled` | The delivery was put on a route. |
| `stop.en_route` | The driver is heading to this stop next. |
| `stop.delivered` | Delivered. Sent after the business's undo window, so a mis-tap undone in time never reaches you. |
| `stop.failed` | The driver couldn't deliver; `reason` says why. |
| `stop.skipped` | Skipped for now; the delivery goes back to OPEN to be routed again. |
| `stop.undone` | An outcome you already received was reversed; `undone_status` says which. |
| `stop.corrected` | The office corrected a finished stop's recorded outcome after the route. |
| `order.unassigned` | A scheduled delivery was taken off its dispatched route and is back to OPEN. |
| `order.updated` | The delivery's details changed in Porch Route (not through your own API call). |
| `order.cancelled` | The delivery was cancelled in Porch Route. |
| `route.completed` | The driver finished the route. |
| `route.cancelled` | The route was cancelled; its deliveries are back to OPEN. |

Answer 2xx quickly. 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 is not retried. A reconciliation sweep reports outcomes still owed, and the Integrations screen can retry any delivery by hand. Proof urls in payloads are signed and expire at `urls_expire_at`.

## Errors

Errors return `{ statusCode, message, details? }` with a sentence you can show a person. `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.

Human-readable documentation: https://porchroutes.com/docs/api. Porch Route integration guide in the repository: docs/INTEGRATION.md.