---
title: "Update Route by ID"
method: PUT
path: "/routes/{id}"
tags: ["Routes"]
---

# Update Route by ID

`PUT /routes/{id}`

## Request

- Base URL: `https://api.smartroutes.io/v2`
- URL: `https://api.smartroutes.io/v2/routes/{id}`
- Auth: none declared

Update a route based on ID. This is a partial update: send only the fields to change, and omitted fields are left as they are. `is_locked` is currently the only editable field.

`is_locked` is a stored flag only. This API does not enforce it: a locked route can still be reversed, dispatched and have orders added or removed.

Returns the updated route in the same shape as `GET /routes/{id}`.

## Path parameters

- `id` integer, required

## Request body

- object
  - `is_locked` boolean: Mark the route as locked (`true`) or unlocked (`false`).

## Response `200`

The route was updated. Setting a field to the value it already has is still a 200.

- RouteResponse
  - `route` Route, required: A route belonging to a plan.
    - `id` integer
    - `plan_id` integer, nullable
    - `name` string
    - `dispatched` boolean
    - `is_locked` boolean: Whether the route is marked as locked. Set with `PUT /routes/{id}`. A stored flag only: this API does not enforce it, so a locked route can still be reversed, dispatched and have orders added or removed.
    - `date` string, date
    - `start_time` string: HH:mm:ss
    - `end_time` string: HH:mm:ss
    - `started_ts` string, date-time, nullable
    - `completed_ts` string, date-time, nullable
    - `total_time` number
    - `travel_time` number
    - `planned_distance` number
    - `actual_distance` number, nullable
    - `vehicle` VehicleRef: Trimmed vehicle reference embedded in Route responses.
      - `vehicle_id` integer
      - `name` string
      - `driver` string, nullable
    - `stops` Stop[]
      - `id` integer
      - `route_id` integer
      - `address` string
      - `postcode` string
      - `phone` string, nullable
      - `lat` number
      - `lng` number
      - `planned_distance` number
      - `arrival_date` string, date
      - `departure_date` string, date
      - `estimated_arrival_time` string: HH:mm or HH:mm:ss
      - `estimated_departure_time` string: HH:mm or HH:mm:ss
      - `actual_completed_ts` string, date-time, nullable
      - `actual_completed_lat` number, nullable
      - `actual_completed_lng` number, nullable
      - `delivery_notes` string, nullable
      - `driver_notes` string, nullable
      - `actual_arrival_time` string, nullable
      - `actual_duration` number, nullable
      - `signature` string, uri, nullable
      - `signature_lat` number, nullable
      - `signature_lng` number, nullable
      - `signed_by` string, nullable
      - `photos` string[], nullable: URLs to proof-of-delivery photos for this stop. A photo appears here as soon as it is uploaded and synced, which may be before compression has finished — following the URL of a photo that is still compressing returns `202 Accepted` with a `Retry-After` header (see the photo endpoint), so be prepared to retry. `null` when there are none.
      - `tracking_link` string, uri, nullable
      - `vehicle_assignment` object, nullable
        - `vehicle_id` integer
      - `directions` object[]
        - `instruction` string
      - `driver_questionnaire_submissions` DriverQuestionnaireSubmission[]
        - `id` integer
        - `driver_questionnaire` object
          - `type` string
          - `title` string
      - `failed_reason` object, nullable
        - `reason` string
      - `tags` string[]
      - `available_days` integer[]
      - `emails` object[], nullable
        - `email` string
      - `driver_phone_calls` object[]: Calls the driver placed to this stop's contact number from the driver app, oldest first. `[]` when there are none. **These are call *attempts*, not call records.** The app hands a `tel:` URL to the device's dialler and the OS takes over, so SmartRoutes never observes the call itself. There is deliberately no status, no duration and no answered/unanswered flag — that information does not exist. Inbound calls, and calls placed outside the driver app, are not recorded at all. `initiated_at` is the driver device's own clock at the moment the call was placed; `recorded_at` is when our servers received it. Drivers work offline, so these can differ by hours, and a call can arrive with an `initiated_at` well before the `recorded_at` of calls you have already seen. Poll on `recorded_at`, not `initiated_at`, if you are looking for new activity. Absence is not proof no call was made: on iOS, a call placed with no GPS fix available is not recorded.
        - `initiated_at` string, date-time, nullable: When the driver placed the call, from the device clock. `null` if the app did not record a usable timestamp.
        - `recorded_at` string, date-time, nullable: When our servers received the record. Later than `initiated_at` — by hours if the driver was working offline.
        - `lat` number, nullable: Driver's latitude when the call was placed. `null` when no GPS fix was available.
        - `lng` number, nullable: Driver's longitude when the call was placed. `null` when no GPS fix was available.
      - `orders` object[]
        - `id` integer
        - `order_number` string
        - `custom_fields` CustomField[], nullable
          - `id` integer
          - `name` string
          - `value` unknown
        - `line_items` LineItem[]
          - `id` integer: Server-set line item id. Ignored on input.
          - `product_name` string
          - `product_code` string
          - `product_quantity` number, float
          - `quantity_outstanding` number, float, nullable: Quantity still to be fulfilled on this line item — left to deliver on a `delivery` order, left to collect on a `pickup` order. Equal to `product_quantity` before anything is fulfilled, a value in between after a partial fulfillment, `0` when exactly the ordered quantity has been fulfilled, and **negative** when more than the ordered quantity was fulfilled. Subtract it from `product_quantity` for the amount the driver has fulfilled. **Read-only:** drivers record fulfillment in the SmartRoutes mobile app; it cannot be set through this API. Notes: `null` — not `0` — when the line item is not tracked, so treat null as unknown rather than as fully fulfilled; an over-fulfillment is reported as recorded rather than clamped; stays at `product_quantity` on `shipment` orders, which are dual-leg and not yet tracked. A value still equal to `product_quantity` does not distinguish an untouched line item from one a driver attended and fulfilled nothing on — read `status` for that (`UNDELIVERED` means attended).
          - `capacities` Capacity[]
            - `id` integer
            - `type` string
            - `capacity` number, nullable
          - `status` 'BOOKED' | 'BOOKING_DECLINED' | 'CANCEL' | 'COMPLETED' | 'DECLINED' | 'DELIVERED' | 'LOADED' | 'NOT RECEIVED' | 'ON HOLD' | 'OPEN' | 'PARTIALLY_DELIVERED' | 'READY_FOR_COLLECTION' | 'RETURNED' | 'ROUTED' | 'UNDELIVERED' | 'WAITING_CONFIRMATION': Per-line-item status. `PARTIALLY_DELIVERED` appears when a line item has been partly fulfilled — partly collected on a `pickup` order or partly delivered on a `delivery` order; how much is left to fulfil is in `quantity_outstanding`. There is no separate picked-up status: completed and partial pickups both use the `DELIVERED` family. `UNDELIVERED` covers both "attended, nothing fulfilled" (in which case `quantity_outstanding` still equals `product_quantity`) and a failed visit. The other `PARTIALLY_*` values are aggregates and apply only at the order level (see `OrderStatus`). Historical rows may still carry legacy values (`CANCEL`, `COMPLETED`, `DECLINED`, `NOT RECEIVED`, `ON HOLD`, `RETURNED`); these are listed for backward compatibility but are no longer written by current SR code.
          - `date` string, date, nullable
          - `custom_fields` CustomField[]
            - `id` integer
            - `name` string
            - `value` unknown
        - `type` string
    - `driver_questionnaire_submissions` DriverQuestionnaireSubmission[]
      - `id` integer
      - `driver_questionnaire` object
        - `type` string
        - `title` string
    - `created` string, date-time
    - `updated` string, date-time

## Other responses

- `400`: Invalid input. The request body failed validation.
- `403`: Forbidden. Returned for rate-limit exhaustion (`name = FORBIDDEN`, with `details.retry_after_seconds` and `Retry-After` header) and permission gate failures (`name = PERMISSION`). Inspect `name` to disambiguate. Send `x-sr-api-features: rate-limit-429` to receive `429 RATE_LIMIT_EXCEEDED` for the rate-limit case instead, leaving `403` to mean permission failures only — see [API Features](#section/Getting-started/API-Features).
- `404`: The requested resource was not found.
- `429`: Rate limit exceeded (`name = RATE_LIMIT_EXCEEDED`). **Only returned to callers that opt in** with the `x-sr-api-features: rate-limit-429` request header; without it the same condition returns `403` with `name = FORBIDDEN` and an otherwise identical body. See [API Features](#section/Getting-started/API-Features).
- `500`: An unexpected server error occurred. The body carries only `name` and `description`: internal details are never returned.

---

[API](https://skmtc.dev/smartroutes/apis/smartroutes-api.md) · [All operations](https://skmtc.dev/smartroutes/apis/smartroutes-api/llms.txt) · [OpenAPI document](https://skmtc.dev/smartroutes/apis/smartroutes-api/revisions/a7ea507afac7?raw)
