openapi: 3.0.3
info:
  title: Dash Open API
  version: "1.0.0"
  description: |
    RESTful API for providers to quote, book, track, and cancel deliveries on
    the Dash Electric fleet. Pair with the Webhook API for real-time delivery
    status updates.

    **Authentication.** Exchange `client_key` + `client_secret` at
    `/v1/auth/providers/token` for a bearer token, then send
    `Authorization: Bearer <token>` on every other request.

    **Environments.** Sandbox and Production share the same host
    (`https://api.dashelectric.co`); the credential pair you exchange selects
    the environment.

    See https://docs.dashelectric.co for full documentation, error reference,
    and webhook payload examples.
  contact:
    name: Dash Engineering
    url: https://docs.dashelectric.co
  license:
    name: Internal use only

servers:
  - url: https://api.dashelectric.co
    description: Production + Sandbox (differentiated by credential pair)

tags:
  - name: Auth
    description: Token exchange
  - name: Deliveries
    description: Quote, create, read, cancel
  - name: Driver
    description: Side-band driver notifications

security:
  - bearerAuth: []

paths:

  # ─── Auth ──────────────────────────────────────────────────────────────

  /v1/auth/providers/token:
    post:
      tags: [Auth]
      summary: Get access token
      description: |
        Exchange your `client_key` + `client_secret` (issued from the Dash
        Next Portal) for a bearer token. Include the returned `token` as
        `Authorization: Bearer <token>` on every other endpoint.
      operationId: getToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            examples:
              default:
                value:
                  client_key: SAMPLE_CLIENT
                  client_secret: random_and_unique_secret_key
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  value:
                    token: random_and_unique_token
        '400':
          description: Bad Request — field validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FieldValidationError'
              examples:
                emptyKey:
                  value:
                    status: 400
                    message: "Terjadi kesalahan pada isian"
                    errors:
                      - field: client_key
                        message: "Bidang client_key tidak boleh kosong"
        '401':
          description: Unauthorized — credentials rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              examples:
                default:
                  value:
                    status: 401
                    message: "Tidak punya akses"

  # ─── Quote ─────────────────────────────────────────────────────────────

  /v1/deliveries/quotes:
    post:
      tags: [Deliveries]
      summary: Get delivery quote
      description: |
        Returns the available service tiers, fares (IDR), distance, and
        estimated pickup / dropoff times for a pickup–dropoff pair. Quotes
        are returned as an array; if you omit `serviceType` the cheapest
        single tier is assumed.
      operationId: getQuote
      parameters:
        - $ref: '#/components/parameters/XClientTimeZone'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
            examples:
              express:
                summary: EXPRESS · INSTANT
                value:
                  serviceCategory: EXPRESS
                  serviceType: INSTANT
                  paymentMethod: CASHLESS
                  packages:
                    - name: Martabak Manis
                      quantity: 1
                      weight: 0.8
                  origin:
                    address: "Manis Cafe & Eatery"
                    coordinates:
                      latitude: -6.993894
                      longitude: 110.457499
                  destination:
                    address: "ALEX AC MOBIL SEMARANG"
                    coordinates:
                      latitude: -6.998216
                      longitude: 110.456340
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
        '400':
          description: Unknown field in body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
              examples:
                badField:
                  value: { status: Failed, error: "Field tidak dikenali: [KEY_NAME]" }
        '404':
          description: Service type not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
              examples:
                unknownService:
                  value: { status: Failed, error: "Data tipe layanan tidak ditemukan" }
        '422':
          description: Distance SLA exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
              examples:
                slaExceeded:
                  value: { status: Failed, error: "Lokasi pengantaran terlalu jauh." }

  # ─── Create delivery (EXPRESS) ─────────────────────────────────────────

  /express/v1/deliveries:
    post:
      tags: [Deliveries]
      summary: Create delivery (EXPRESS)
      description: |
        Places a booking on the Dash Express fleet. Returns a `deliveryID`
        in status `ALLOCATING`. The driver is assigned asynchronously —
        track progress via webhooks.

        The legacy `POST /v1/deliveries` path is deprecated 21 May 2026;
        new integrations must use `/express/v1/deliveries`. LOGISTIC
        deliveries continue to use the legacy path.
      operationId: createDelivery
      parameters:
        - $ref: '#/components/parameters/XClientTimeZone'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeliveryRequest'
            examples:
              cashless:
                summary: CASHLESS EXPRESS INSTANT
                value:
                  providerOrderID: AWB-1042
                  serviceCategory: EXPRESS
                  serviceType: INSTANT
                  paymentMethod: CASHLESS
                  packages:
                    - name: Martabak Manis
                      quantity: 1
                      weight: 0.8
                  sender: { firstName: Arifatul, phone: "081234548899" }
                  recipient: { firstName: Tony, phone: "082186789900" }
                  origin:
                    address: "Manis Cafe & Eatery"
                    coordinates:
                      latitude: -6.993894
                      longitude: 110.457499
                  destination:
                    address: "ALEX AC MOBIL SEMARANG"
                    coordinates:
                      latitude: -6.998216
                      longitude: 110.456340
              cashOnDelivery:
                summary: CASH on Delivery
                value:
                  providerOrderID: AWB-1043
                  serviceCategory: EXPRESS
                  serviceType: INSTANT
                  paymentMethod: CASH
                  cashOnDelivery: { amount: 250000 }
                  packages:
                    - name: Martabak Manis
                      quantity: 1
                      weight: 0.8
                  sender: { firstName: Arifatul, phone: "081234548899" }
                  recipient: { firstName: Tony, phone: "082186789900" }
                  origin:
                    address: "Manis Cafe & Eatery"
                    coordinates: { latitude: -6.993894, longitude: 110.457499 }
                  destination:
                    address: "ALEX AC MOBIL SEMARANG"
                    coordinates: { latitude: -6.998216, longitude: 110.456340 }
      responses:
        '201':
          description: Delivery created in ALLOCATING
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '400':
          description: Out of operational hours / area, or bad field
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                outOfHours:
                  value: { status: Failed, error: "Layanan gagal karena di luar waktu atau area operasional." }
        '404':
          description: Service type not enabled for your workspace
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                unknownService:
                  value: { status: Failed, error: "Jenis layanan tidak dikenali." }
        '422':
          description: Distance SLA exceeded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                slaExceeded:
                  value: { status: Failed, error: "Lokasi pengantaran terlalu jauh." }

  # ─── Get + Cancel delivery ─────────────────────────────────────────────

  /v1/deliveries/{deliveryID}:
    parameters:
      - $ref: '#/components/parameters/DeliveryID'
    get:
      tags: [Deliveries]
      summary: Get delivery details
      description: |
        Returns the current state of an in-progress or completed delivery —
        status, courier, quote, timeline history, and POD media.
      operationId: getDelivery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Delivery' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                notFound:
                  value: { status: Failed, error: "Pengantaran tidak ditemukan" }
    delete:
      tags: [Deliveries]
      summary: Cancel delivery
      description: |
        Cancels a delivery while it is still in `ALLOCATING` or
        `PENDING_PICKUP`. Once the driver has started pickup, provider-side
        cancellation is no longer allowed.
      operationId: cancelDelivery
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
                  description: Optional cancellation reason surfaced to Dash Ops.
                  example: Customer changed mind
      responses:
        '200':
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: Success }
                  message: { type: string, example: "Pengantaran berhasil dibatalkan." }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
        '409':
          description: Conflict — already terminal, or driver already picked up
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                alreadyFinal:
                  value: { status: Failed, error: "Perubahan status antaran tidak dapat dilakukan." }
                alreadyPickedUp:
                  value: { status: Failed, error: "Pengantaran sudah diambil" }

  # ─── Notify driver ─────────────────────────────────────────────────────

  /v1/deliveries/{deliveryID}/additional-notifications:
    parameters:
      - $ref: '#/components/parameters/DeliveryID'
    post:
      tags: [Driver]
      summary: Notify driver
      description: |
        Sends a side-band notification to the assigned driver — currently
        used to signal that the package is physically ready for pickup.
        Allowed only while status is `PENDING_PICKUP` or `PICKING_UP`.
      operationId: notifyDriver
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type:
                  type: string
                  enum: [READY_TO_PICKUP]
                  example: READY_TO_PICKUP
      responses:
        '200':
          description: Notification sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: Success }
                  message: { type: string, example: "Notifikasi berhasil dikirim" }
        '400':
          description: Bad request (unknown type / no courier / wrong status)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }
              examples:
                unknownType:
                  value: { status: Failed, error: "Field `type` tidak dikenali" }
                noCourier:
                  value: { status: Failed, error: "Kurir belum ditugaskan untuk antaran ini." }
                wrongStatus:
                  value: { status: Failed, error: "Notifikasi tidak dapat dikirim karena status pengantaran tidak sesuai." }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SimpleError' }

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Bearer token obtained from `POST /v1/auth/providers/token`. Send as
        `Authorization: Bearer <token>` on every endpoint except the token
        endpoint itself.

  parameters:
    XClientTimeZone:
      name: x-client-time-zone
      in: header
      required: true
      description: Timezone of the requesting client; affects scheduling.
      schema:
        type: string
        enum:
          - Asia/Jakarta
          - Asia/Makassar
          - Asia/Jayapura
        example: Asia/Jakarta

    DeliveryID:
      name: deliveryID
      in: path
      required: true
      description: Dash-issued opaque ID, prefixed `DE-`.
      schema:
        type: string
        example: DE-1727921269869

  schemas:

    Coordinates:
      type: object
      required: [latitude, longitude]
      properties:
        latitude:
          type: number
          format: double
          example: -6.993894
        longitude:
          type: number
          format: double
          example: 110.457499

    Address:
      type: object
      required: [address, coordinates]
      properties:
        address: { type: string, example: "Manis Cafe & Eatery" }
        coordinates: { $ref: '#/components/schemas/Coordinates' }
        province: { type: string }
        city: { type: string }
        district: { type: string }
        subDistrict: { type: string }
        postalCode: { type: string }
        notes: { type: string, description: Free-form note shown to the driver }

    Package:
      type: object
      required: [name, quantity, weight]
      properties:
        name: { type: string, example: "Martabak Manis" }
        description: { type: string, example: "kacang dan keju" }
        quantity: { type: integer, example: 1 }
        price:
          type: integer
          description: Declared total value in IDR (not unit price). Used for insurance and COD.
          example: 23000
        weight: { type: number, example: 0.8, description: kg }
        width: { type: number, description: cm }
        length: { type: number, description: cm }
        height: { type: number, description: cm }

    Contact:
      type: object
      required: [firstName]
      properties:
        firstName: { type: string, example: Arifatul }
        lastName: { type: string }
        companyName: { type: string }
        email: { type: string, format: email }
        phone: { type: string, example: "081234548899" }

    Recipient:
      allOf:
        - $ref: '#/components/schemas/Contact'
        - type: object
          properties:
            actualRecipientName:
              type: string
              nullable: true
              description: Name of the actual person who signed for the package. Populated on COMPLETED.

    Courier:
      type: object
      nullable: true
      properties:
        name: { type: string, example: Agus Wijayanto }
        phone: { type: string, example: "087777777777" }

    Currency:
      type: object
      properties:
        code: { type: string, example: IDR }
        symbol: { type: string, example: Rp }

    Quote:
      type: object
      properties:
        service:
          type: object
          properties:
            category:
              type: string
              enum: [EXPRESS, LOGISTIC]
            type:
              type: string
              enum: [INSTANT, SAME_DAY]
            name:
              type: string
              example: Dash Instant Delivery
        currency: { $ref: '#/components/schemas/Currency' }
        amount:
          type: integer
          description: Total fare in IDR.
          example: 9000
        estimatedTimeline:
          type: object
          properties:
            pickup: { type: string, format: date-time, nullable: true }
            dropoff: { type: string, format: date-time, nullable: true }
        distance:
          type: number
          description: Distance in meters.
          example: 499

    Schedule:
      type: object
      properties:
        pickupTimeFrom:
          type: string
          format: date-time
          example: "2025-05-26T23:00:00+07:00"
        pickupTimeTo:
          type: string
          format: date-time
          example: "2025-05-26T23:59:59+07:00"

    Instruction:
      type: object
      properties:
        type:
          type: string
          enum: [WEBVIEW]
        text: { type: string }
        url: { type: string }

    Media:
      type: object
      properties:
        type:
          type: string
          enum: [PICKUP_PROOF, DELIVERY_PROOF, FAILED_PROOF]
        url: { type: string }
        createdAt: { type: string, format: date-time }

    TimelineEntry:
      type: object
      properties:
        status: { type: string }
        createdAt: { type: string, format: date-time }

    TokenRequest:
      type: object
      required: [client_key, client_secret]
      properties:
        client_key:
          type: string
          example: SAMPLE_CLIENT
        client_secret:
          type: string
          example: random_and_unique_secret_key

    TokenResponse:
      type: object
      properties:
        token:
          type: string
          example: random_and_unique_token

    QuoteRequest:
      type: object
      required: [packages, origin, destination]
      properties:
        serviceCategory:
          type: string
          enum: [EXPRESS, LOGISTIC]
          default: EXPRESS
        serviceType:
          type: string
          enum: [INSTANT, SAME_DAY]
        paymentMethod:
          type: string
          enum: [CASH, CASHLESS]
          default: CASHLESS
        packages:
          type: array
          items: { $ref: '#/components/schemas/Package' }
        origin: { $ref: '#/components/schemas/Address' }
        destination: { $ref: '#/components/schemas/Address' }
        schedule: { $ref: '#/components/schemas/Schedule' }

    QuoteResponse:
      type: object
      properties:
        quotes:
          type: array
          items: { $ref: '#/components/schemas/Quote' }
        packages:
          type: array
          items: { $ref: '#/components/schemas/Package' }
        origin: { $ref: '#/components/schemas/Address' }
        destination: { $ref: '#/components/schemas/Address' }
        locationAvailability: { type: boolean }
        driverAvailability: { type: boolean }
        availability: { type: boolean }

    CreateDeliveryRequest:
      type: object
      required:
        - providerOrderID
        - serviceCategory
        - serviceType
        - packages
        - sender
        - recipient
        - destination
      properties:
        providerOrderID:
          type: string
          description: Your AWB / order reference. Echoed in every webhook.
          example: AWB-1042
        serviceCategory:
          type: string
          enum: [EXPRESS, LOGISTIC]
        serviceType:
          type: string
          enum: [INSTANT, SAME_DAY]
        paymentMethod:
          type: string
          enum: [CASH, CASHLESS]
          default: CASHLESS
        cashOnDelivery:
          type: object
          description: Required when paymentMethod = CASH.
          properties:
            amount:
              type: integer
              description: COD amount in IDR.
        packages:
          type: array
          items: { $ref: '#/components/schemas/Package' }
        sender: { $ref: '#/components/schemas/Contact' }
        recipient: { $ref: '#/components/schemas/Contact' }
        origin: { $ref: '#/components/schemas/Address' }
        destination: { $ref: '#/components/schemas/Address' }
        instructions:
          type: array
          maxItems: 1
          items: { $ref: '#/components/schemas/Instruction' }
        schedule: { $ref: '#/components/schemas/Schedule' }

    Delivery:
      type: object
      properties:
        deliveryID: { type: string, example: DE-1727921269869 }
        providerOrderID: { type: string, example: AWB-1042 }
        providerOutletID: { type: string, example: Outlet Tebet }
        paymentMethod: { type: string, enum: [CASH, CASHLESS] }
        status:
          type: string
          enum:
            - QUEUEING
            - PREPARING
            - VERIFIED
            - NOT_VERIFIED
            - ALLOCATING
            - PENDING_PICKUP
            - PICKING_UP
            - PENDING_DELIVERY
            - IN_DELIVERY
            - COMPLETED
            - FAILED
            - CANCELLED
        courier: { $ref: '#/components/schemas/Courier' }
        trackingURL:
          type: string
          nullable: true
          description: EXPRESS only. Present from IN_DELIVERY onward.
        sender: { $ref: '#/components/schemas/Contact' }
        recipient: { $ref: '#/components/schemas/Recipient' }
        quote: { $ref: '#/components/schemas/Quote' }
        media:
          type: array
          items: { $ref: '#/components/schemas/Media' }
        timeline:
          type: array
          items: { $ref: '#/components/schemas/TimelineEntry' }
        createdAt: { type: string, format: date-time }
        pickupAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
        failedAt: { type: string, format: date-time, nullable: true }
        failedReason: { type: string, nullable: true }
        cancelledAt: { type: string, format: date-time, nullable: true }
        cancelledReason: { type: string, nullable: true }

    # ─── Error envelopes ────────────────────────────────────────────────

    SimpleError:
      type: object
      description: |
        Standard error envelope used by most delivery endpoints — a status
        string ("Failed") and an Indonesian-language `error` message you
        can switch on.
      properties:
        status:
          type: string
          example: Failed
        error:
          type: string
          example: "Pengantaran tidak ditemukan"

    AuthError:
      type: object
      description: Auth endpoint error envelope (numeric status + message).
      properties:
        status: { type: integer, example: 401 }
        message: { type: string, example: "Tidak punya akses" }

    FieldValidationError:
      type: object
      description: Per-field validation error from the auth endpoint.
      properties:
        status: { type: integer, example: 400 }
        message: { type: string, example: "Terjadi kesalahan pada isian" }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string, example: client_key }
              message: { type: string, example: "Bidang client_key tidak boleh kosong" }
