My Optimized Route

REST API Reference

Base URL: https://myoptimizedroute.com/api/v1

Authentication

All API requests require a Bearer token in the Authorization header. Generate API keys from Settings → API Keys in your client portal.

Example Request

curl https://myoptimizedroute.com/api/v1/customers \
  -H "Authorization: Bearer mor_live_your32hexcharshere"
Live vs. Test keys: Both key types authenticate against your real data and behave identically. mor_test_ keys are a labeling convention. They help you identify traffic from development or staging environments in the API request log. There is no separate sandbox environment.

401 Unauthorized

Missing, invalid, or revoked API key.

429 Too Many Requests

Rate limit exceeded. See header Retry-After.

Rate Limit

60 requests per minute per API key.

Response Format

All responses are JSON with the following envelope:

{
  "data":  <object | array | null>,
  "error": null | { "message": "Description of the error" },
  "meta":  {} | { "page": 1, "per_page": 50, "total": 243 }
}

HTTP status codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict, 422 Unprocessable, 429 Rate Limited.

Errors

All error responses follow the standard envelope format with data: null and an error.message describing the issue.

Status Meaning Common Causes
400 Bad Request Missing required fields or invalid field values. Check error.message for specifics.
401 Unauthorized Missing, invalid, or revoked API key. Verify your Authorization header is correct and the key has not been revoked in Settings → API Keys.
403 Forbidden Your plan does not include API access. API access requires a Pro or Enterprise plan.
404 Not Found The requested resource does not exist or does not belong to your tenant.
409 Conflict A visible customer with the same address already exists. Use PATCH to update the existing record.
422 Unprocessable The request was understood but could not be processed. Common causes: monthly stop limit reached, invalid stop status transition, driver does not belong to your tenant.
429 Too Many Requests Rate limit exceeded. Check the Retry-After header for how long to wait before retrying.
500 Internal Server Error An unexpected server error occurred. If this persists, contact support.

Example 422 Response

{
  "data": null,
  "error": { "message": "Monthly stop limit reached. Upgrade your plan to continue creating routes." },
  "meta": {}
}

Customers

GET /api/v1/customers — List customers

Returns a paginated list of customers in your address library.

Query Parameters

NameTypeDefaultDescription
page integer 1 Page number.
per_page integer 50 Results per page (max 100).
search string Filter by name or address (partial match).

Response 200 OK

[
  {
    "id": 42,
    "customer_name": "Jane Smith",
    "first_name": "Jane",
    "last_name": "Smith",
    "address": "123 Main St",
    "address2": "",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701",
    "email": "jane@example.com",
    "phone": "555-1234",
    "geocode_verified": true,
    "created_at": "2026-01-15 09:30:00"
  }
]
POST /api/v1/customers — Create customer

Adds a customer to your address library. Returns the existing customer if a hidden match is found (unhide-on-match). Returns 409 if a visible customer with the same address already exists.

Request Body (JSON)

{
  "address":       "123 Main St",    // required
  "city":          "Springfield",    // required
  "state":         "IL",             // required
  "zip":           "62701",          // required
  "customer_name": "Jane Smith",     // optional
  "first_name":    "Jane",           // optional (preferred over customer_name)
  "last_name":     "Smith",          // optional
  "email":         "jane@example.com",
  "phone":         "555-1234",
  "notes":         "Gate code: 1234"
}

Response 201 Created

{ "id": 42, "customer_name": "Jane Smith", ... }
GET /api/v1/customers/{id} — Get customer

Returns a single customer by ID.

Response 200 OK

{ "id": 42, "customer_name": "Jane Smith", ... }
PATCH /api/v1/customers/{id} — Update customer

Updates any subset of customer fields. If address fields change, geocode_verified is reset to false.

Request Body (JSON)

{
  "first_name": "Janet",
  "phone":      "555-9999"
}

Response 200 OK

{ "id": 42, "customer_name": "Janet Smith", ... }

Routes

GET /api/v1/routes — List routes

Returns a paginated list of routes for your tenant.

Query Parameters

NameTypeDescription
pageintegerPage number (default 1).
per_pageintegerResults per page (max 100, default 50).
statusstringFilter by: planned, active, completed, cancelled, incomplete.
dateYYYY-MM-DDFilter by route date.

Response 200

[
  {
    "id": 7,
    "driver_id": 3,
    "driver_name": "Alex Johnson",
    "route_date": "2026-03-30",
    "status": "completed",
    "fixed_order": false,
    "stop_total": 8,
    "stop_delivered": 7,
    "stop_failed": 1,
    "started_at": "2026-03-30 09:15:00",
    "completed_at": "2026-03-30 12:45:00",
    "created_at": "2026-03-29 18:00:00"
  }
]
POST /api/v1/routes — Create route

Creates a new planned route with stops. Enforces the monthly stop limit.

Request Body (JSON)

{
  "driver_id":   3,           // required — must belong to your tenant
  "route_date":  "2026-04-01", // required — YYYY-MM-DD
  "fixed_order": false,        // optional — driver cannot re-optimize if true
  "stops": [                   // required — at least one stop
    { "customer_id": 42, "deliverables": "2x Crackers" },
    { "customer_id": 18 }
  ]
}

Response 201 Created

{
  "id": 12, "status": "planned", "stop_total": 2,
  "stops": [
    { "stop_id": 101, "stop_order": 1, "status": "not_started",
      "customer_id": 42, "customer_name": "Jane Smith",
      "address": "123 Main St", "deliverables": "2x Crackers", ... },
    { "stop_id": 102, "stop_order": 2, "status": "not_started",
      "customer_id": 18, "customer_name": "Bob Lee", ... }
  ]
}
GET /api/v1/routes/{id} — Get route with stops

Returns a single route with its full stop list including customer details and timing.

Response 200

{
  "id": 7,
  "driver_id": 3,
  "driver_name": "Alex Johnson",
  "route_date": "2026-04-01",
  "status": "active",
  "fixed_order": false,
  "stop_total": 3,
  "stop_delivered": 1,
  "stop_failed": 0,
  "started_at": "2026-04-01 09:15:00",
  "completed_at": null,
  "created_at": "2026-03-31 18:00:00",
  "stops": [
    {
      "stop_id": 101,
      "stop_order": 1,
      "status": "delivered",
      "customer_id": 42,
      "customer_name": "Jane Smith",
      "address": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701",
      "deliverables": "2x Crackers",
      "driver_notes": "Left at door",
      "en_route_at": "2026-04-01 09:20:00",
      "arrived_at": "2026-04-01 09:31:00",
      "completed_at": "2026-04-01 09:34:00",
      "pod_captured": true
    },
    {
      "stop_id": 102,
      "stop_order": 2,
      "status": "en_route",
      "customer_id": 18,
      "customer_name": "Bob Lee",
      "address": "456 Oak Ave",
      "city": "Springfield",
      "state": "IL",
      "zip": "62702",
      "deliverables": "1x Beef Formula",
      "driver_notes": null,
      "en_route_at": "2026-04-01 09:38:00",
      "arrived_at": null,
      "completed_at": null,
      "pod_captured": false
    },
    {
      "stop_id": 103,
      "stop_order": 3,
      "status": "not_started",
      "customer_id": 27,
      "customer_name": "Maria Santos",
      "address": "789 Elm St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62703",
      "deliverables": null,
      "driver_notes": null,
      "en_route_at": null,
      "arrived_at": null,
      "completed_at": null,
      "pod_captured": false
    }
  ]
}

Stops

Stop Object

Stop objects are returned as part of GET /api/v1/routes/{id} and POST /api/v1/routes responses under the stops array.

Field Type Description
stop_idintegerUnique stop identifier.
stop_orderintegerPosition in the route sequence (1-based).
statusstringnot_started, en_route, arrived, delivered, failed
customer_idintegerID of the customer in your address library.
customer_namestringFull name of the customer.
addressstringStreet address.
citystringCity.
statestringState abbreviation.
zipstringZIP / postal code.
deliverablesstring | nullFree-text dispatcher note describing what is being delivered.
driver_notesstring | nullNotes recorded by the driver at the time of delivery.
en_route_atdatetime | nullTimestamp when the driver marked the stop as en route.
arrived_atdatetime | nullTimestamp when the driver marked arrival.
completed_atdatetime | nullTimestamp when the stop reached a terminal status (delivered or failed).
pod_capturedbooleanWhether proof of delivery (photo or signature) was collected.
Valid status transitions:
not_started → en_route → arrived → delivered
arrived → failed
PATCH /api/v1/routes/{route_id}/stops/{stop_id} — Update stop status

Advances a stop through its status lifecycle. Fires delivery notifications (email/SMS) the same as the driver portal. Marks the route completed when all stops reach a terminal status.

Valid transitions: not_started → en_route → arrived → delivered or arrived → failed

Request Body (JSON)

{
  "new_status": "delivered",      // required
  "notes":      "Left at door"    // optional — shown to dispatcher
}

Deliveries

GET /api/v1/deliveries — List deliveries

Returns individual stop-level delivery records across all routes.

Query Parameters

NameTypeDescription
page / per_pageintegerPagination.
statusstringdelivered, failed, or processing (in-progress).
date_fromYYYY-MM-DDFilter route_date ≥ this date.
date_toYYYY-MM-DDFilter route_date ≤ this date.
driver_idintegerFilter by driver.

Response 200

[
  {
    "id": 55,
    "customer_name": "Jane Smith",
    "address": "123 Main St",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701",
    "driver_name": "Alex Johnson",
    "status": "delivered",
    "deliverables": "2x Crackers",
    "en_route_at": "2026-03-30 10:00:00",
    "arrived_at": "2026-03-30 10:12:00",
    "completed_at": "2026-03-30 10:15:00",
    "notes": "Left at door",
    "route_id": 7,
    "route_date": "2026-03-30",
    "pod_captured": true
  }
]

Rate Limits

Each API key is limited to 60 requests per minute. The counter resets at the start of each new 60-second window. When exceeded, a 429 Too Many Requests response is returned with a Retry-After header indicating how many seconds to wait.

HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/json

{
  "data": null,
  "error": { "message": "Rate limit exceeded. Maximum 60 requests per minute." },
  "meta": {}
}