openapi: 3.0.3

# Single source of truth for the Hizliyo Vendor API.
# The documentation pages are written by hand for readability, but every field, code and
# constraint must agree with this file. When they disagree, this file wins.

info:
  title: Hizliyo Vendor API
  version: "1.0.0"
  description: |
    Send a delivery request from your POS; a courier company under contract with Hizliyo
    picks the package up and delivers it.

    Hizliyo is a software provider, not a carrier. Delivery is performed by the fleet the
    restaurant has an agreement with. Status updates are informational, not a guarantee.
  contact:
    name: Hizliyo Integration Support
    url: https://developers.hizliyo.com

servers:
  - url: https://gw.hizliyo.com/api/vendor/v1
    description: Production
  - url: https://gw-dev.hizliyo.com/api/vendor/v1
    description: Sandbox

security:
  - bearerAuth: []

tags:
  - name: Authentication
  - name: Stores
  - name: Orders

paths:
  /token:
    post:
      tags: [Authentication]
      summary: Exchange credentials for a bearer token
      description: |
        The token is valid for one hour. Cache it and renew about five minutes before expiry —
        a token that expires mid-request causes the order to be rejected, and rejected orders
        are never retried by Hizliyo.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [clientId, clientSecret]
              properties:
                clientId:
                  type: string
                  example: adisyo
                clientSecret:
                  type: string
                  example: hzl_live_a7f39c21e8b4d5f60c1a2b3d4e5f6071
      responses:
        "200":
          description: Token issued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          accessToken: { type: string }
                          tokenType: { type: string, example: Bearer }
                          expiresIn: { type: integer, example: 3600 }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /stores/{externalStoreId}:
    put:
      tags: [Stores]
      summary: Create or update a store, attach a fleet
      description: |
        Upsert keyed by your own store id. Omit `fleetId` on the first call and the response
        returns the fleets that serve this address in `availableFleets` — there is no separate
        discovery endpoint.

        PATCH is blocked at the edge layer and will not reach the API. Always use PUT.
      parameters:
        - name: externalStoreId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
          example: ADS-34781
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StoreRequest"
      responses:
        "200":
          description: Store created or updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/StoreResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: |
            One channel per branch. `ALREADY_HIZLIYO_PARTNER` — the branch is already a direct
            Hizliyo partner. `STORE_EXISTS_ON_ANOTHER_VENDOR` — registered by another integrator.
            `DUPLICATE_STORE` — you already registered this branch under a different store id.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"

  /orders:
    post:
      tags: [Orders]
      summary: Create a delivery request
      description: |
        Synchronous. Every precondition — fleet suspended, courier availability, dropoff
        coverage, store configuration — is evaluated before the response returns. If the order
        is not accepted it is not created, and Hizliyo does not retry it in the background.
      parameters:
        - name: X-Idempotency-Key
          in: header
          required: true
          schema: { type: string, maxLength: 64 }
          description: Stable per order. Same key with the same body returns the original response.
          example: ADS-ORD-99213
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderRequest"
      responses:
        "201":
          description: Delivery created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/OrderCreated"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "`STORE_NOT_CONFIGURED` or `DUPLICATE_ORDER`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "423":
          description: "`FLEET_SUSPENDED` or `FLEET_TERMINATED` — the store needs another fleet"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: "`FLEET_UNAVAILABLE` — no courier right now. Retryable after ~60 s"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /orders/{orderId}:
    get:
      tags: [Orders]
      summary: Current status and full timeline
      description: |
        Limited to one request per order per 10 seconds. Status is delivered by webhook; this
        endpoint is the fallback after downtime. There is no bulk or changed-since variant.
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Current state
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/OrderStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: "`ORDER_NOT_FOUND`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          $ref: "#/components/responses/RateLimited"

  /orders/{orderId}/cancel:
    post:
      tags: [Orders]
      summary: Cancel before pickup
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  enum: [CustomerCancelled, RestaurantCancelled, AddressProblem, Other]
                note:
                  type: string
                  maxLength: 250
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          orderId: { type: string, format: uuid }
                          status: { type: string, example: Cancelled }
                          cancelledAt: { type: string, format: date-time }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "`CANCEL_TOO_LATE` — the courier already collected the package"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Envelope:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        message: { type: string }

    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        message:
          type: string
          description: Human-readable. May be reworded — branch on errorCode instead.
        data:
          type: object
          properties:
            errorCode:
              type: string
              enum:
                - TOKEN_EXPIRED
                - INVALID_CREDENTIALS
                - STORE_NOT_CONFIGURED
                - ALREADY_HIZLIYO_PARTNER
                - STORE_EXISTS_ON_ANOTHER_VENDOR
                - DUPLICATE_STORE
                - INVALID_TAX_NUMBER
                - FLEET_SUSPENDED
                - FLEET_TERMINATED
                - FLEET_UNAVAILABLE
                - DROPOFF_OUT_OF_COVERAGE
                - COORDINATE_MISMATCH
                - INVALID_RECIPIENT
                - INVALID_PHONE
                - INVALID_ADDRESS
                - READY_AT_TOO_FAR
                - DUPLICATE_ORDER
                - CANCEL_TOO_LATE
                - ORDER_NOT_FOUND
                - RATE_LIMITED
            retryable: { type: boolean }
            action: { type: string }
            retryAfterSeconds: { type: integer }

    Address:
      type: object
      required: [text, latitude, longitude]
      properties:
        text: { type: string, minLength: 10 }
        latitude: { type: number, format: double }
        longitude: { type: number, format: double }

    StoreRequest:
      type: object
      required: [taxNumber, name, phone, address]
      properties:
        taxNumber:
          type: string
          description: Identifies the branch; enforces one channel per branch.
          example: "1234567890"
        name: { type: string, example: "Oncu Doner - Kizilay" }
        phone:
          type: string
          description: 10 digits. Leading 0 and +90 are stripped.
          example: "5321234567"
        address:
          allOf:
            - $ref: "#/components/schemas/Address"
          description: Pickup point. The courier navigates to these coordinates.
        fleetId:
          type: string
          format: uuid
          description: Omit until the restaurant has chosen a fleet.

    StoreResponse:
      type: object
      properties:
        status:
          type: string
          enum: [AwaitingFleet, AwaitingFleetApproval, Active, Migrating, Suspended, Deactivated]
        subRegion: { type: string }
        fleet:
          $ref: "#/components/schemas/FleetRef"
        availableFleets:
          type: array
          description: Present when no fleet is attached. Replaces a separate discovery endpoint.
          items:
            $ref: "#/components/schemas/FleetOption"

    FleetRef:
      type: object
      properties:
        fleetId: { type: string, format: uuid }
        name: { type: string }

    FleetOption:
      allOf:
        - $ref: "#/components/schemas/FleetRef"
        - type: object
          properties:
            status:
              type: string
              enum: [Available, AtCapacity, Unavailable]
            coverage:
              type: array
              items: { type: string }
              description: Sub-region names only. Polygons are not exposed.
            avgDeliveryMinutes: { type: integer }

    OrderRequest:
      type: object
      required: [externalStoreId, externalOrderId, recipient, dropoff]
      properties:
        externalStoreId: { type: string, example: ADS-34781 }
        externalOrderId:
          type: string
          maxLength: 64
          description: Unique within your account.
          example: ADS-ORD-99213
        orderedAt:
          type: string
          format: date-time
          description: ISO-8601. Without an offset, UTC+3 is assumed.
        readyAt:
          type: string
          format: date-time
          description: |
            When the package leaves the kitchen — the courier is timed against this. Defaults
            to now. Past values are pulled to now; more than 4 hours ahead is rejected.
        recipient:
          type: object
          required: [name, phone]
          properties:
            name: { type: string, minLength: 2 }
            phone: { type: string, example: "5339876543" }
        dropoff:
          allOf:
            - $ref: "#/components/schemas/Address"
            - type: object
              properties:
                buildingNo: { type: string }
                doorNo: { type: string }
                floor: { type: string }
                note: { type: string }
          description: |
            Coordinates must be inside Turkey, otherwise the order is rejected with
            COORDINATE_MISMATCH. Serviceability is checked separately against the fleet
            coverage area (DROPOFF_OUT_OF_COVERAGE). The courier navigates to these
            coordinates, not to the address text — send the point your own map resolved.
        packageNote:
          type: string
          maxLength: 500
        orderAmount:
          type: number
          format: double
          description: Stored for reconciliation and reporting. Not Hizliyo revenue.
        payment:
          type: object
          description: |
            Stored and passed to the courier. Hizliyo runs no collection workflow — there is no
            "money received" confirmation anywhere in the flow.
          properties:
            type:
              type: string
              enum: [Cash, Card, Prepaid]
            collectionAmount:
              type: number
              format: double

    OrderCreated:
      type: object
      properties:
        orderId: { type: string, format: uuid }
        externalOrderId: { type: string }
        status: { type: string, example: Searching }
        fleet: { $ref: "#/components/schemas/FleetRef" }
        trackingUrl: { type: string, format: uri }
        estimatedPickupAt: { type: string, format: date-time }

    OrderStatus:
      type: object
      description: |
        Identical in shape to the `data` object of a delivery.status_changed webhook, plus the
        events array. One parser covers both.
      properties:
        orderId: { type: string, format: uuid }
        externalOrderId: { type: string }
        status:
          $ref: "#/components/schemas/DeliveryStatus"
        courier:
          type: object
          properties:
            name:
              type: string
              description: Surname abbreviated.
              example: Emre K.
            phone: { type: string }
            vehicleType: { type: string }
        location:
          type: object
          properties:
            latitude: { type: number, format: double }
            longitude: { type: number, format: double }
        etaDropoffAt: { type: string, format: date-time }
        failureReason: { type: string }
        updatedAt: { type: string, format: date-time }
        events:
          type: array
          items:
            type: object
            properties:
              status: { $ref: "#/components/schemas/DeliveryStatus" }
              at: { type: string, format: date-time }

    DeliveryStatus:
      type: string
      enum:
        - Searching
        - Assigned
        - PickingUp
        - PickedUp
        - Delivering
        - Delivered
        - Failed
        - Cancelled

  responses:
    Unauthorized:
      description: |
        `TOKEN_EXPIRED` — renew and resend once. `INVALID_CREDENTIALS` — do not retry.
        The WWW-Authenticate header distinguishes them.
      headers:
        WWW-Authenticate:
          schema: { type: string }
          example: Bearer error="invalid_token", error_description="expired"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    ValidationFailed:
      description: Field-level validation failure
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  # Outbound notifications. Documented here for completeness; delivery is HTTP POST to the
  # URL you register, signed with HMAC-SHA256 over the raw body in X-Hizliyo-Signature.
  x-webhooks:
    events:
      - delivery.status_changed
      # The fleet accepted the store: from this point orders are accepted.
      - store.activated
      # The fleet declined. The store keeps working but has no fleet — call the store
      # endpoint again and pick another one from availableFleets.
      - store.fleet_rejected
      - store.migrating
      - store.deactivated
      - fleet.payment_overdue
    headers:
      X-Hizliyo-Signature: sha256=<hex>
      X-Hizliyo-Event: <event name>
      X-Hizliyo-Attempt: <integer>
    retry: exponential backoff for up to 24 hours
