openapi: 3.1.0
info:
  title: CargoMov Partner API
  version: 1.0.0
  description: Server-to-server delivery, tracking, invoice and webhook API for approved CargoMov business integrations.
servers:
  - url: https://europe-west1-cargomov-ghana.cloudfunctions.net/cargoMovPartnerApi
    description: CargoMov partner gateway. The credential prefix selects the isolated test environment or live operations.
security:
  - bearerApiKey: []
  - headerApiKey: []
paths:
  /jobs:
    post:
      operationId: createJob
      summary: Create a delivery request
      description: Requires the jobs:create scope and a unique Idempotency-Key header.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
      responses:
        '201':
          description: Job created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobMutationResponse'
        '200':
          description: Idempotent replay; the existing job was returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobMutationResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /jobs/{jobId}:
    get:
      operationId: getJob
      summary: Read an owned job and its current tracking state
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Safe job status
          content:
            application/json:
              schema:
                type: object
                required: [ok, job]
                properties:
                  ok: { const: true }
                  job: { $ref: '#/components/schemas/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /jobs/{jobId}/cancel:
    post:
      operationId: cancelJob
      summary: Request cancellation of an owned job
      parameters:
        - $ref: '#/components/parameters/JobId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  minLength: 4
                  maxLength: 500
      responses:
        '200':
          description: Idempotent replay; the existing cancellation request was returned
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JobMutationResponse' }
        '202':
          description: Cancellation request recorded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JobMutationResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /invoices/{invoiceId}:
    get:
      operationId: getInvoice
      summary: Read an owned invoice
      parameters:
        - name: invoiceId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Safe invoice record
          content:
            application/json:
              schema:
                type: object
                required: [ok, invoice]
                properties:
                  ok: { const: true }
                  invoice: { $ref: '#/components/schemas/Invoice' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /webhooks:
    get:
      operationId: listWebhookEndpoints
      summary: List webhook endpoints owned by the credential account
      responses:
        '200':
          description: Webhook endpoints
          content:
            application/json:
              schema:
                type: object
                required: [ok, endpoints]
                properties:
                  ok: { const: true }
                  endpoints:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookEndpoint' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      operationId: createWebhookEndpoint
      summary: Register a public HTTPS webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  pattern: '^https://'
                events:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: { $ref: '#/components/schemas/WebhookEventType' }
      responses:
        '201':
          description: Endpoint registered for destination authorization. The signingSecret is returned once; no outbound request is sent yet.
          content:
            application/json:
              schema:
                type: object
                required: [ok, endpointId, url, events, signingSecret]
                properties:
                  ok: { const: true }
                  endpointId: { type: string }
                  url: { type: string, format: uri }
                  events:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookEventType' }
                  environment: { type: string, enum: [test, live] }
                  signingSecret: { type: string }
                  secretPrefix: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /webhooks/{endpointId}:
    delete:
      operationId: deleteWebhookEndpoint
      summary: Disable an owned webhook endpoint
      parameters:
        - $ref: '#/components/parameters/EndpointId'
      responses:
        '200':
          description: Endpoint disabled
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OkResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /webhooks/{endpointId}/test:
    post:
      operationId: testWebhookEndpoint
      summary: Queue a webhook test event without transmitting it
      parameters:
        - $ref: '#/components/parameters/EndpointId'
      responses:
        '202':
          description: Test event queued for inspection; outbound transmission is not active
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OkResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: CargoMov API key
    headerApiKey:
      type: apiKey
      in: header
      name: X-CargoMov-Api-Key
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 100
        pattern: '^[A-Za-z0-9._:-]{8,100}$'
    JobId:
      name: jobId
      in: path
      required: true
      schema: { type: string }
    EndpointId:
      name: endpointId
      in: path
      required: true
      schema: { type: string }
  responses:
    BadRequest:
      description: Invalid input
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing, invalid, expired or revoked API key
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: The credential lacks the required scope
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Record is unavailable or is not owned by this account
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: The requested state transition is not allowed
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Credential rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    OkResponse:
      type: object
      required: [ok]
      properties:
        ok: { const: true }
    Error:
      type: object
      required: [ok, error]
      properties:
        ok: { const: false }
        error: { type: string }
        message: { type: string }
        requestId: { type: string }
    CreateJobRequest:
      type: object
      required: [pickupLocation, dropoffLocation, cargoType, requiredVehicleType]
      properties:
        pickupLocation: { type: string, minLength: 1, maxLength: 500 }
        dropoffLocation: { type: string, minLength: 1, maxLength: 500 }
        cargoType: { type: string, minLength: 1, maxLength: 120 }
        cargoDetails: { type: string, maxLength: 2000 }
        requiredVehicleType: { type: string, minLength: 1, maxLength: 120 }
        budget: { type: number, minimum: 0 }
        pricingMode: { type: string, enum: [fixedPrice, driverOffers] }
        pickupStops: { type: array, maxItems: 20, items: { type: object } }
        deliveryStops: { type: array, maxItems: 20, items: { type: object } }
        declaredCargoValue: { type: number, minimum: 0 }
        insuranceRequested: { type: boolean }
        purchaseOrderNumber: { type: string, maxLength: 100 }
        costCentre: { type: string, maxLength: 100 }
        branchId: { type: string, maxLength: 100 }
    JobMutationResponse:
      type: object
      required: [ok, jobId]
      properties:
        ok: { const: true }
        jobId: { type: string }
        duplicate: { type: boolean }
        status: { type: string }
    Job:
      type: object
      required: [id, status]
      properties:
        id: { type: string }
        status: { type: string }
        tripStatus: { type: string }
        pickupLocation: { type: string }
        dropoffLocation: { type: string }
        vehicleType: { type: string }
        amount: { type: number }
        eta: { type: string }
        updatedAt: { type: integer, description: Unix time in milliseconds }
    Invoice:
      type: object
      required: [id, status, total, currency]
      properties:
        id: { type: string }
        reference: { type: string }
        jobId: { type: string }
        status: { type: string }
        subtotal: { type: number }
        total: { type: number }
        currency: { type: string, example: GHS }
        issuedAt: { type: integer }
        dueAt: { type: integer }
        paidAt: { type: integer }
    WebhookEventType:
      type: string
      enum:
        - job.created
        - job.updated
        - job.completed
        - job.cancelled
        - tracking.updated
        - delivery.created
        - delivery.updated
        - delivery.completed
        - delivery.cancelled
        - payment.succeeded
        - payment.failed
        - payment.refunded
        - payment.updated
        - payment.confirmed
        - invoice.created
        - invoice.updated
        - invoice.paid
        - invoice.overdue
    WebhookEndpoint:
      type: object
      required: [endpointId, url, events, status]
      properties:
        endpointId: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEventType' }
        status: { type: string, enum: [Active, Disabled, Deleted] }
        environment: { type: string, enum: [test, live] }
        deliveryEnabled: { type: boolean, const: false }
        activationStatus: { type: string, example: Destination Authorization Required }
        consecutiveFailures: { type: integer }
        createdAt: { type: integer }
        lastSuccessAt: { type: integer }
        lastFailureAt: { type: integer }
