> ## Documentation Index
> Fetch the complete documentation index at: https://developers.kardinal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Retrieve the latest state of a plan

> Returns the most recent entry in the plan's state history: a single summary value (`waiting`, `processing`, `preOptimizing`, and so on — see `PlanState`). For the detailed per-stage breakdown (waiting room, creation, optimization, and, if predictive traffic is enabled, traffic fetching), see `PlanStatus` on the plan itself.

> **Modelling pitfall.** The state history outlives the plan for a while: after a `DELETE`, this endpoint can keep answering `200`. The state shows `deleted` only if the plan had finished optimizing; otherwise it moves to `stopped`, then `interrupted`. To know whether a plan still exists, call `GET /plans/{planId}` instead.




## OpenAPI

````yaml /openapi.yaml get /plans/{planId}/state
openapi: 3.0.3
info:
  title: Kardinal Route Optimization API
  version: 1.0.1
  description: >
    This document specifies the REST API of Kardinal for builder users.


    Every endpoint applies to the single agency carried by the access token
    (either the sandbox agency or the production agency), which is why no agency
    id appears in the paths. An access token that does not grant access to
    exactly one agency is rejected with a `403`.


    This single-agency shape is specific to **builder** users, the audience of
    this document. A builder account is created through self-service sign-up and
    always has exactly two agencies, `sandbox` and `production` — one per
    environment. This differs from a **standard** Kardinal user, who may belong
    to a pre-existing set of agencies of any size and receives a single access
    token scoped accordingly, rather than one token per environment.


    A `429` response with an empty body signals a temporary overload, not a
    quota: retry after a pause, with an increasing delay between attempts. See
    [Limits and
    quotas](https://developers.kardinal.ai/reference/limits-and-quotas).


    ## Terminology


    A few everyday terms map to specific fields in this API. Reusing the API's
    own vocabulary in integration code and support requests avoids ambiguity:


    - **Route:** the ordered sequence of stops a single resource performs within
    a plan's solution. Called a `tour` in the API response (`item.tours[]`, with
    `resourceId`, `distanceInKm`, `workingDuration`, and `wayPoints`). "Route"
    is the everyday term; `tour` is the field name you'll see in payloads.

    - **Route plan** (or just "plan"): the full request submitted for
    optimization (`resources`, `orders`, and any plan-level constraints or
    objectives). Called `Plan` in this API. Submitting a plan doesn't return a
    route directly; it returns a `Solution` object (under `item`) containing one
    tour per resource.

    - **Disruption:** an event during execution that invalidates part of an
    already-computed solution and calls for a new one: a delay, a cancellation,
    or an urgent new order, typically. A disruption is handled by re-optimizing
    the affected plan (see "Re-optimization" below), not by starting a new one.

    - **Re-optimization:** submitting an update to a plan that already has a
    solution (`PUT /plans/{planId}`), using the *same* `id` (the `version` is
    incremented, and the plan re-optimized, only if its content changed; a `PUT`
    identical to the stored plan changes nothing and isn't billed). The engine
    treats this as "same problem, updated data" (the update still carries the
    complete plan, not only the changes): it repairs the previous solution,
    adjusting it just enough to be valid for the new data, then keeps improving
    from there, rather than searching from scratch.

    - **Delivery window:** the time window during which a `delivery`-kind stop
    should or must be visited: `authorizedTimeWindows` (hard: outside of it, the
    stop can't be planned at all) or `preferredTimeWindows` (soft: reachable
    outside it, at the cost of the `minimizeDelay` objective).
  contact:
    url: https://kardinal.ai/
    email: customer.success@kardinal.ai
servers:
  - url: https://app.kardinal.ai/api/v2
    description: >-
      Production and sandbox (same host; the API token used determines which
      agency a call applies to).
  - url: /api/v2
    description: Relative path, for environments where the host is supplied separately.
security:
  - access_token: []
tags:
  - name: Plan
    description: How to create, retrieve, update, and delete plans.
  - name: Solution
    description: How to retrieve the solution of a plan and its objectives.
  - name: Webhook
    description: >-
      How to register, retrieve, update, and delete plan webhooks, and inspect
      their delivery results.
paths:
  /plans/{planId}/state:
    parameters:
      - $ref: '#/components/parameters/planId'
    get:
      tags:
        - Plan
      summary: Retrieve the latest state of a plan
      description: >
        Returns the most recent entry in the plan's state history: a single
        summary value (`waiting`, `processing`, `preOptimizing`, and so on — see
        `PlanState`). For the detailed per-stage breakdown (waiting room,
        creation, optimization, and, if predictive traffic is enabled, traffic
        fetching), see `PlanStatus` on the plan itself.


        > **Modelling pitfall.** The state history outlives the plan for a
        while: after a `DELETE`, this endpoint can keep answering `200`. The
        state shows `deleted` only if the plan had finished optimizing;
        otherwise it moves to `stopped`, then `interrupted`. To know whether a
        plan still exists, call `GET /plans/{planId}` instead.
      operationId: getLastPlanState
      responses:
        '200':
          description: Latest plan state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnvelopedTimedPlanState'
              examples:
                minimalPlan:
                  summary: Latest state for the minimal plan example
                  value:
                    item:
                      planVersion: 1
                      timestamp: '2026-03-21T08:00:05Z'
                      state: optimized
                    agencyId: BLD1234567_production
                    planId: 4cbd0ab8-282c-4b30-b981-29e1ed8a2016
        '401':
          $ref: '#/components/responses/NotAuthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  parameters:
    planId:
      name: planId
      description: The plan UUID.
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/UUID'
  schemas:
    EnvelopedTimedPlanState:
      type: object
      description: A single plan state-history entry wrapped with its identifying metadata.
      properties:
        item:
          description: The state-history entry itself.
          allOf:
            - $ref: '#/components/schemas/TimedPlanState'
        agencyId:
          allOf:
            - $ref: '#/components/schemas/AgencyId'
        planId:
          $ref: '#/components/schemas/PlanId'
    UUID:
      type: string
      format: uuid
      description: Universally Unique Identifier.
      pattern: >-
        ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
      example: cd4ce4e3-0208-4b10-b346-25f235214e4f
      readOnly: true
    TimedPlanState:
      type: object
      readOnly: true
      description: >-
        A snapshot of a plan's `PlanState` at a given point in time, one entry
        in its state history.
      properties:
        planVersion:
          description: The corresponding plan's version.
          example: 2
          allOf:
            - $ref: '#/components/schemas/PlanVersion'
        timestamp:
          description: When this state was recorded.
          allOf:
            - $ref: '#/components/schemas/DateTime'
        state:
          description: The plan's state at `timestamp`.
          allOf:
            - $ref: '#/components/schemas/PlanState'
    AgencyId:
      type: string
      description: >
        Identifier of the Kardinal tenant/account the plan is scoped to, derived
        automatically from the access token used to make the request.


        > **Modelling pitfall.** Not a physical-depot or site partition key. A
        single fleet based at several physical depots is still one `Plan`; do
        not split it into several `Plan` objects just because resources have
        different `departure`/`arrival` positions.
      readOnly: true
      pattern: ^BLD[0-9]{7}_[a-zA-Z0-9-._~:@!$,]+$
      example: BLD1234567_production
    PlanId:
      description: The plan id.
      readOnly: true
      example: 4cbd0ab8-282c-4b30-b981-29e1ed8a2016
      allOf:
        - $ref: '#/components/schemas/UUID'
    EnvelopedErrors:
      type: object
      description: >-
        Envelope wrapping one or more business errors, returned on every error
        response regardless of HTTP status, except a `429` temporary overload,
        which comes with an empty body.
      properties:
        agencyId:
          allOf:
            - $ref: '#/components/schemas/AgencyId'
          description: >
            The agency the failed action was scoped to.


            > **Default.** Absent when the action isn't tied to any agency at
            all — for example a `401` `NOT_AUTHENTICATED` response, returned
            before any agency could be resolved from the token.
        planId:
          allOf:
            - $ref: '#/components/schemas/PlanId'
          description: >
            The plan the failed action was scoped to.


            > **Default.** Absent whenever the action concerns an agency but no
            specific plan, or when `agencyId` itself is absent.
        errors:
          type: array
          description: >-
            The business errors that occurred. A `400` on plan or resource
            creation can report several invalid fields at once.
          items:
            allOf:
              - $ref: '#/components/schemas/Error'
        warnings:
          type: array
          description: >-
            Amendments applied to the payload before the preceding errors were
            found, if any.
          items:
            allOf:
              - $ref: '#/components/schemas/Warning'
      example:
        errors:
          - code: INVALID_VALUE
            message: The field value is not valid.
    PlanVersion:
      type: integer
      description: The plan version.
      readOnly: true
      minimum: 1
      example: 42
    DateTime:
      type: string
      description: >-
        A full calendar date time, expressed in the ISO8601 **date** format:
        YYYY-MM-DDThh:mm:ssZ.
      example: '2019-11-15T12:34:56Z'
    PlanState:
      type: string
      readOnly: true
      description: >
        The corresponding plan's state.


        - waiting: The plan was received and is awaiting processing.

        - processing: The plan is being processed.

        - preOptimizing: The plan is being optimized while awaiting traffic or
        other information.

        - preOptimized: While still awaiting traffic or other information, one
        of the following events has occurred: no better solution can be
        produced, or the optimization period has reached its limit. Note that
        'preOptimized' should be followed by 'optimizing' and 'optimized'.

        - optimizing: The plan is being optimized with all required information.

        - optimized: This state can be triggered by one of the following events:
        no better solution can be produced, or the optimization period has
        reached its limit.

        - stopped: The plan's awaiting optimizations were cancelled.

        - deleted: The plan was deleted and awaiting optimizations were
        cancelled.

        - interrupted: The plan was either updated, stopped, or deleted during
        its optimization.
      enum:
        - waiting
        - processing
        - preOptimizing
        - preOptimized
        - optimizing
        - optimized
        - stopped
        - deleted
        - interrupted
    Error:
      type: object
      readOnly: true
      description: >
        A single business error. Every error response, whatever the HTTP status,
        wraps one or more `Error` objects in an `EnvelopedErrors` envelope; a
        single response (typically a `400` on plan/resource creation) can report
        several invalid fields at once. The one exception is a `429` temporary
        overload, which comes with an empty body.


        **Always branch your error handling on `code`, not on the HTTP status
        alone or on this response's `description` in this spec.** Several `code`
        values can share the same status. In particular, all of `INVALID_INPUT`,
        `ID_NOT_UNIQUE`, `KEYS_NOT_UNIQUE`, `INVALID_ID_REFERENCE`,
        `INVALID_VALUE`, `PRECONDITION_FAILED`, and `NOT_IMPLEMENTED` return
        `400`.


        A code's status in the following table applies when that code is
        returned on its own. A plan that exceeds a size limit of its agency
        returns a `403` carrying one `INVALID_VALUE` per exceeded limit, each
        naming the field (`properties.path`) and the limit
        (`properties.details`), and a single `NOT_ALLOWED`. Read every error in
        the response, not only the first.


        ## Business error codes


        | `code`                  | HTTP status | Default message | Returned
        when |

        | ------------------------ | ----------- | ---------------- |
        ------------- |

        | `INVALID_INPUT`          | 400 | The payload cannot be parsed. | The
        request body isn't valid JSON, or doesn't match the expected schema
        (wrong type, missing required field). |

        | `ID_NOT_UNIQUE`          | 400 | The payload contains a collection
        with an id repeated multiple times. | A collection in the payload (for
        example `resources`, `orders`) has the same `id` on more than one item.
        |

        | `KEYS_NOT_UNIQUE`        | 400 | The payload contains a collection
        with the keys repeated multiple times. | A collection has a repeated
        business key other than `id` (for example a tag or reference field
        expected to be unique). |

        | `INVALID_ID_REFERENCE`   | 400 | The id reference contains an id that
        does not exist. | A field references an `id` that doesn't match anything
        else in the payload or system (for example a `resourceId` that doesn't
        exist). |

        | `INVALID_VALUE`          | 400 | The field value is not valid. | A
        field's value fails a validation rule: format, range, or allowed values.
        |

        | `PRECONDITION_FAILED`    | 400 | A precondition failed. | The action
        requires the target resource to be in a particular state, and it isn't
        (for example acting on an already-deleted or already-archived object). |

        | `NOT_IMPLEMENTED`        | 400 | Not yet implemented. | The requested
        behaviour is recognized but not available yet. Despite the name, this
        returns `400`, not `501`; treat it as a client-facing "not supported"
        rather than a server capability gap. |

        | `NOT_AUTHENTICATED`      | 401 | The caller is not authenticated. | No
        access token was sent, or it's missing, malformed, or expired. |

        | `NOT_ALLOWED`            | 403 | The requested action is not allowed.
        | An agency limit was reached: an optimization quota, a plan size limit,
        or insufficient credits. `properties.details` states which one. |

        | `NOT_FOUND`              | 404 | The requested object could not be
        found. | The resource addressed by the URL doesn't exist, or isn't
        visible to the caller (the API doesn't distinguish the two, to avoid
        leaking existence of resources you can't access). |

        | `INTERNAL_SERVER_ERROR`  | 500 | The server encountered an unexpected
        condition that prevented it from fulfilling the request. | Unexpected
        server-side failure, unrelated to the request's content. If this
        persists, contact support with the request's timestamp. |


        This enum reflects the codes returned by the current server
        implementation. New codes may be added in future non-breaking releases;
        treat an unrecognized `code` value defensively (fall back to the HTTP
        status and `message`) rather than assuming this list is closed.


        One thing this catalogue does not cover: **infeasible plans are not an
        error.** Submitting a plan the engine can't fully satisfy still returns
        a normal `200`/`201`. The plan is accepted, and unplanned stops or
        constraint violations show up in the solution (`unaffectedStopIds`,
        `violations`) rather than as an `Error`.
      properties:
        code:
          type: string
          description: >
            Stable identifier for the error type. Branch your error handling on
            this field, not on the HTTP status alone or on this response's
            `description` in this spec. Several `code` values can share the same
            status. See the preceding table.
          enum:
            - INVALID_INPUT
            - ID_NOT_UNIQUE
            - KEYS_NOT_UNIQUE
            - INVALID_ID_REFERENCE
            - INVALID_VALUE
            - PRECONDITION_FAILED
            - NOT_IMPLEMENTED
            - NOT_AUTHENTICATED
            - NOT_ALLOWED
            - NOT_FOUND
            - INTERNAL_SERVER_ERROR
          example: INVALID_VALUE
        message:
          type: string
          description: >-
            Default, human-readable message. Useful for logs; not meant to be
            parsed.
          example: The field value is not valid.
        properties:
          allOf:
            - $ref: '#/components/schemas/ErrorProperties'
      required:
        - message
        - code
    Warning:
      readOnly: true
      description: >
        A warning has the same structure as an `Error`, but a different
        semantic. It is present when an accepted payload was amended — traffic
        turned off, crow-fly forced, `maxOptimizationDuration` clamped, and so
        on. The amended field and the applied change are described by
        `properties.path` and `properties.details`.


        Warnings can also accompany the errors of a rejected payload: they then
        describe the amendments applied before the rejecting error was found.


        > **Modelling pitfall.** Warnings are only returned in the response to
        the `POST` or `PUT` that produced them. They aren't stored with the
        plan, so a later `GET` never returns them: read them from that response,
        since afterwards only the amended value remains visible.
      allOf:
        - $ref: '#/components/schemas/Error'
    ErrorProperties:
      type: object
      description: >
        Optional extra context for the error, when the API provides any.


        The exact keys returned in `properties` for a given `code` (for example,
        which field name or invalid value is included in an `INVALID_VALUE` or
        `INVALID_ID_REFERENCE` error) are not yet documented per `code`. Treat
        any `properties` content as informational and not stable.
      additionalProperties:
        type: string
  responses:
    NotAuthenticated:
      description: >
        The caller is not authenticated (`code: NOT_AUTHENTICATED`): no access
        token was sent, or it's missing, malformed, or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EnvelopedErrors'
          examples:
            notAuthenticated:
              summary: NOT_AUTHENTICATED
              value:
                errors:
                  - code: NOT_AUTHENTICATED
                    message: The caller is not authenticated.
    Forbidden:
      description: >
        The caller is not allowed to perform this action (`code: NOT_ALLOWED`):
        for a builder account, this currently always means that an agency limit
        was reached, whether an optimization quota, a plan size limit, or
        insufficient credits. `properties.details` states which one. See [Limits
        and quotas](https://developers.kardinal.ai/reference/limits-and-quotas).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EnvelopedErrors'
          examples:
            dailyQuotaExceeded:
              summary: NOT_ALLOWED — daily optimization quota reached
              value:
                agencyId: BLD1234567_sandbox
                planId: f7ee9aa5-36e5-4ce5-a978-8d69892f7831
                errors:
                  - code: NOT_ALLOWED
                    message: The requested action is not allowed.
                    properties:
                      details: >-
                        The maximum number of optimizations per day (1500) has
                        been reached for agency BLD1234567_sandbox
            insufficientCredits:
              summary: NOT_ALLOWED — insufficient credits
              value:
                agencyId: BLD1234567_production
                planId: f7ee9aa5-36e5-4ce5-a978-8d69892f7831
                errors:
                  - code: NOT_ALLOWED
                    message: The requested action is not allowed.
                    properties:
                      details: Insufficient credits
                      remaining: '17'
            agencyQuotaConfigViolation:
              summary: NOT_ALLOWED — plan size limit exceeded
              value:
                agencyId: BLD1234567_sandbox
                planId: f7ee9aa5-36e5-4ce5-a978-8d69892f7831
                errors:
                  - code: INVALID_VALUE
                    message: The field value is not valid.
                    properties:
                      details: >-
                        The plan's number of resources (251) is greater than the
                        allowed value (250) for agency BLD1234567_sandbox
                      path: resources
                  - code: NOT_ALLOWED
                    message: The requested action is not allowed.
                    properties:
                      details: >-
                        The resulting plan violates the quotas configuration of
                        the agency
    NotFound:
      description: >
        The specified resource was not found (`code: NOT_FOUND`): the resource
        addressed by the URL doesn't exist, or isn't visible to the caller (the
        API doesn't distinguish the two, to avoid leaking the existence of
        resources the caller can't access).


        On `GET /plans/{planId}/solution`, only `properties.details` tells two
        cases apart: `Plan not found` means the plan doesn't exist or was
        deleted; `Solution version not found.` means the plan exists but has no
        solution yet, as while it's `waiting` or `processing`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EnvelopedErrors'
          examples:
            notFound:
              summary: NOT_FOUND
              value:
                errors:
                  - code: NOT_FOUND
                    message: The requested object could not be found.
    InternalServerError:
      description: >
        An internal server error has occurred (`code: INTERNAL_SERVER_ERROR`):
        an unexpected server-side failure, unrelated to the request's content.
        If this persists, contact support with the request's timestamp.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EnvelopedErrors'
          examples:
            internalServerError:
              summary: INTERNAL_SERVER_ERROR
              value:
                errors:
                  - code: INTERNAL_SERVER_ERROR
                    message: >-
                      The server encountered an unexpected condition that
                      prevented it from fulfilling the request.
  securitySchemes:
    access_token:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        JWT bearer token, sent as `Authorization: Bearer <token>`. Two kinds of
        token are accepted:


        - A **session token** (1 hour), obtained through the interactive Console
        login flow. Used to sign in to the Console; not meant to be used against
        this API.

        - A **long-term API token** (366 days), obtained once via the Console
        and meant to be stored by your integration. It is the credential
        recommended for backend use, since it does not depend on repeating the
        interactive login flow.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.