openapi: 3.1.0
info:
  title: Govza Partner Delivery API
  version: "1.0.0"
  description: |
    Create and track Govza deliveries from your own system.

    An API key is issued by a Govza customer from their dashboard, and every
    request made with it acts as that customer — the orders you create are their
    orders, priced and billed exactly as if they had entered them by hand.

    ## Addresses

    Govza does **not** geocode. Every address must arrive already structured and
    already carrying coordinates: `latitude`, `longitude`, `area`, `street` and
    `building` are all required. The coordinates decide routing and price; the
    text is what the courier reads. If your system cannot produce coordinates,
    it cannot use this API.

    `apartment`, `entrance` and `floor` must be whole numbers. Anything else
    (`"12A"`, `"корп. 3"`) belongs in `address.instructions`, which is free text
    shown to the courier.

    ## Idempotency

    `external_order_id` is your own identifier for the order and is the
    idempotency key. Repeating a create with an id we have already seen returns
    `200` with the original order rather than creating a second delivery, so a
    retry after a timeout is always safe.
servers:
  - url: https://api.govza.app/v1/partner
security:
  - apiKey: []
tags:
  - name: Orders
  - name: Diagnostics

paths:
  /ping:
    get:
      tags: [Diagnostics]
      summary: Verify a key
      description: Confirms the key works and reports which account it acts as.
      responses:
        "200":
          description: The key is valid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PingResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /quotes:
    post:
      tags: [Orders]
      summary: Price a delivery
      description: |
        Returns the price, distance and duration for a trip without creating
        anything. Pass the returned `quote_id` to `POST /orders` to lock in
        these prices; quotes expire after five minutes, after which the order is
        re-priced at the current rate.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QuoteRequest"
      responses:
        "200":
          description: The trip was priced.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuoteResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /orders:
    post:
      tags: [Orders]
      summary: Create a delivery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "201":
          description: The delivery was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponse"
        "200":
          description: |
            This `external_order_id` was already used; the original order is
            returned unchanged and nothing new was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: A concurrent request is already creating this order. Retry shortly.
        "429":
          $ref: "#/components/responses/RateLimited"

  /orders/{external_order_id}:
    get:
      tags: [Orders]
      summary: Read a delivery
      parameters:
        - $ref: "#/components/parameters/ExternalOrderId"
      responses:
        "200":
          description: The current state of the delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /orders/{external_order_id}/cancel:
    post:
      tags: [Orders]
      summary: Cancel a delivery
      parameters:
        - $ref: "#/components/parameters/ExternalOrderId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 255
                comment:
                  type: string
                  maxLength: 2000
      responses:
        "200":
          description: The delivery was cancelled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

webhooks:
  orderStatusChanged:
    post:
      summary: Order status changed
      description: |
        Sent to the webhook URL configured on your key whenever the delivery
        reaches a new state.

        Each request carries an `X-Govza-Signature` header of the form
        `t=<unix seconds>,v1=<hex>`, where the hex is
        `HMAC-SHA256(secret, "<t>.<raw request body>")`. Verify it against the
        webhook secret shown when the key was created, and reject requests whose
        `t` is more than a few minutes old.

        Respond `2xx` to acknowledge. Anything else is retried with exponential
        backoff for up to eight attempts.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Acknowledged.

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer gvz_<key id>_<secret>` — the key issued in the
        Govza dashboard. It is shown once, at creation.

  parameters:
    ExternalOrderId:
      name: external_order_id
      in: path
      required: true
      description: Your own identifier for the order.
      schema:
        type: string
        maxLength: 128

  responses:
    Unauthorized:
      description: The key is missing, malformed, revoked, or its account can no longer create orders.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: No order exists for this `external_order_id`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimited:
      description: Too many requests for this key in the current minute.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: |
        The payload was rejected. `error.details` is a JSON array of
        `{ field, message }` covering **every** offending field, not just the
        first, so one response is enough to fix the request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              id: validation.partner_payload_invalid
              message: "dropoffs.0.address.latitude: latitude is required"
              details: '[{"field":"dropoffs.0.address.latitude","message":"latitude is required"}]'

  schemas:
    Address:
      type: object
      required: [latitude, longitude, area, street, building]
      properties:
        latitude:
          type: number
          minimum: -90
          maximum: 90
          description: Required. Decides routing and price.
        longitude:
          type: number
          minimum: -180
          maximum: 180
        area:
          type: string
          description: City or locality.
          example: Грозный
        street:
          type: string
          example: проспект Путина
        building:
          type: string
          description: House or building number, as text.
          example: "12к3"
        apartment:
          type: integer
          nullable: true
          description: Whole number only. Put anything else in `instructions`.
        entrance:
          type: integer
          nullable: true
        floor:
          type: integer
          nullable: true
        intercom:
          type: string
          nullable: true
          maxLength: 64
        instructions:
          type: string
          nullable: true
          maxLength: 1000
          description: Free text shown to the courier.

    Contact:
      type: object
      required: [value]
      properties:
        value:
          type: string
          description: Phone number. E.164 preferred.
          example: "+79280000001"
        name:
          type: string
          nullable: true
        type:
          type: string
          enum: [phone]
          default: phone

    Pickup:
      type: object
      required: [address, contact]
      properties:
        address:
          $ref: "#/components/schemas/Address"
        contact:
          $ref: "#/components/schemas/Contact"

    Dropoff:
      type: object
      required: [address, contact]
      properties:
        address:
          $ref: "#/components/schemas/Address"
        contact:
          $ref: "#/components/schemas/Contact"
        cargo_size:
          type: string
          enum: [S, M, L, XL]
        payment_type:
          type: string
          enum: [cash, card]
          description: What the courier collects at this door.
        payment_amount:
          type: number
          nullable: true
          description: Omit or null when nothing is owed on delivery.
        doorstep:
          type: boolean
          default: false

    QuoteRequest:
      type: object
      required: [pickup, dropoffs]
      properties:
        pickup:
          $ref: "#/components/schemas/Pickup"
        dropoffs:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: "#/components/schemas/Dropoff"
        routing_mode:
          type: string
          enum: [parallel, sequential]
          default: sequential
          description: |
            `sequential` routes one chained trip through every dropoff in order.
            `parallel` prices each dropoff independently from the pickup.

    RouteLeg:
      type: object
      properties:
        route_id:
          type: string
        distance:
          type: number
          description: Metres.
        duration:
          type: number
          description: Seconds.
        price:
          type: number

    QuoteResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            quote_id:
              type: string
              description: Pass to `POST /orders` to keep these prices.
            price:
              type: number
            distance:
              type: number
            duration:
              type: number
            expires_at:
              type: string
              format: date-time
            dropoffs:
              type: array
              items:
                $ref: "#/components/schemas/RouteLeg"

    CreateOrderRequest:
      allOf:
        - $ref: "#/components/schemas/QuoteRequest"
        - type: object
          required: [external_order_id]
          properties:
            external_order_id:
              type: string
              maxLength: 128
              description: Your identifier. Reusing one returns the original order.
            quote_id:
              type: string
              description: |
                A `quote_id` from `POST /quotes`. Honoured only while the whole
                quote is still valid; otherwise the trip is re-priced and the
                response carries the new price.
            cargo_size:
              type: string
              enum: [S, M, L, XL]
              default: M
            payment_type:
              type: string
              enum: [cash, card]
              default: cash
            payment_amount:
              type: number
              nullable: true
            delivery_time:
              type: string
              format: date-time
              nullable: true
              description: For a scheduled delivery. Omit for as-soon-as-possible.
            comment:
              type: string
              nullable: true
              maxLength: 2000
              description: Shown to the courier.

    Order:
      type: object
      properties:
        external_order_id:
          type: string
        order_code:
          type: string
          description: The human-readable Govza order code.
        status:
          $ref: "#/components/schemas/OrderStatus"
        price:
          type: number
          nullable: true
        tracking_url:
          type: string
          nullable: true
          description: Public tracking page, safe to forward to the recipient.

    OrderStatus:
      type: string
      enum: [new, assigned, in_progress, delivered, cancelled]

    OrderResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: "#/components/schemas/Order"

    OrderDetailResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          allOf:
            - $ref: "#/components/schemas/Order"
            - type: object
              properties:
                status_changes:
                  type: array
                  items:
                    type: object

    PingResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            ok:
              type: boolean
            account:
              type: object
              properties:
                id:
                  type: string
                email:
                  type: string
            scopes:
              type: array
              items:
                type: string
            rate_limit_per_minute:
              type: integer
              nullable: true

    WebhookEvent:
      type: object
      properties:
        event:
          type: string
          enum: [order.assigned, order.in_progress, order.delivered, order.cancelled]
        occurred_at:
          type: string
          format: date-time
        external_order_id:
          type: string
        order_code:
          type: string
        status:
          $ref: "#/components/schemas/OrderStatus"
        price:
          type: number
          nullable: true
        tracking_url:
          type: string
          nullable: true
        driver:
          type: object
          nullable: true
          properties:
            name:
              type: string
              nullable: true
            phone:
              type: string
              nullable: true

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            id:
              type: string
            code:
              type: string
            message:
              type: string
            details:
              type: string
