---
title: "Get Plan by ID"
method: GET
path: "/plans/{id}"
tags: ["Plans"]
---

# Get Plan by ID

`GET /plans/{id}`

## Request

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

Fetch a plan in your depot with its full routes and its `unserved` stops (the stops the optimiser could not fit on any route). Each route includes its vehicle, timings and `stops` in visiting order. Each stop includes its orders and line items, live progress and proof of delivery, in the same shape as `GET /routes/{id}`. A plan's routes are the routes from its current optimisation, plus any route dispatched from an earlier optimisation of the same plan.

While the optimisation started by `POST /plans/optimise/orders` is still running on a new plan, `routes` and `unserved` are both empty arrays. See that endpoint for polling guidance and its limits on failure signals, or poll `GET /plans/optimise/{id}`. To list plans without their stops, use `GET /plans`.

## Path parameters

- `id` number, required

## Response `200`

Successful response

- PlanResponse
  - `plan` Plan, required: A VRP plan. When no solution has been computed yet, `routes` and `unserved` are empty arrays — see the async optimise endpoint description for polling guidance.
    - `id` integer
    - `total_time` number
    - `dispatched` boolean, nullable
    - `routes` Route[]
      - `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[]
              - …
            - `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[]
              - …
          - `type` string
      - `driver_questionnaire_submissions` DriverQuestionnaireSubmission[]
        - `id` integer
        - `driver_questionnaire` object
          - `type` string
          - `title` string
      - `created` string, date-time
      - `updated` string, date-time
    - `unserved` object[]
      - `id` integer
      - `address` string
      - `postcode` string
      - `phone` string
      - `lat` number
      - `lng` number
      - `delivery_notes` string
      - `reference_id` string
      - `available_days` integer[]
    - `created` string, date-time
    - `updated` string, date-time

## Other responses

- `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)
