openapi: 3.1.0
info:
  title: Stablique Payout API
  version: "1.0.0"
  description: |
    One integration, every corridor.

    Institutions in the DRC connect once to Stablique and send international
    payouts. You prefund a USD float; we screen the instruction, debit the
    float at the quoted rate, and route each payout to the licensed partner
    with the best all-in price for that corridor at that moment.

    The flow is always the same:

      1. `POST /v1/quotes`   — price a corridor, lock a rate
      2. `POST /v1/payouts`  — send against that quote
      3. `payout.status`     — webhooks carry it to completion

    Sandbox uses mock partners and simulated settlement. No real money moves.
  contact:
    name: Stablique
    email: developers@stablique.xyz
servers:
  - url: https://api.sandbox.stablique.xyz
    description: Sandbox
  - url: https://api.stablique.xyz
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Quotes
    description: Price a corridor and lock a rate.
  - name: Payouts
    description: Send and track payouts.
  - name: Account
    description: Float balance and corridor coverage.
  - name: Webhooks
    description: Endpoint registration for payout.status events.

paths:
  /v1/corridors:
    get:
      tags: [Account]
      summary: List available corridors
      description: Corridors are configuration on our side. New destinations appear here without a release on yours.
      responses:
        "200":
          description: Corridor list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Corridor" }

  /v1/quotes:
    post:
      tags: [Quotes]
      summary: Create a quote
      description: |
        Prices the instruction with every eligible partner and locks the winning
        rate for the life of the quote. Quotes expire — rates move.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [corridor, amount]
              properties:
                corridor:
                  type: string
                  description: Destination country, ISO 3166-1 alpha-2.
                  example: AE
                amount:
                  type: number
                  description: Amount to send, in USD.
                  example: 25000
                payout_method:
                  type: string
                  enum: [bank_transfer, mobile_money]
                  description: Defaults to the corridor's primary method.
      responses:
        "201":
          description: Quote created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        "400":
          $ref: "#/components/responses/BadRequest"

  /v1/quotes/{id}:
    get:
      tags: [Quotes]
      summary: Retrieve a quote
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Quote
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/payouts:
    post:
      tags: [Payouts]
      summary: Create a payout
      description: |
        Creates a payout against an active quote. Idempotent by `reference`:
        replaying the same reference returns the original payout rather than
        sending twice.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quote_id, reference, beneficiary]
              properties:
                quote_id: { type: string, example: qt_9f1c2b3a4d5e6f70 }
                reference:
                  type: string
                  description: Your reference for this payout. Must be unique.
                  example: DBK-IMP-8801
                beneficiary: { $ref: "#/components/schemas/Beneficiary" }
                metadata:
                  type: object
                  additionalProperties: true
      responses:
        "201":
          description: Payout created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }
        "402":
          description: Insufficient float
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: Quote expired, quote already used, or duplicate reference
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    get:
      tags: [Payouts]
      summary: List payouts
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
        - name: status
          in: query
          schema:
            type: string
            enum: [received, routed, settled, paid_out, failed]
        - name: corridor
          in: query
          schema: { type: string }
        - name: reference
          in: query
          schema: { type: string }
        - name: starting_after
          in: query
          description: Payout id to paginate after.
          schema: { type: string }
      responses:
        "200":
          description: Payout list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  has_more: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Payout" }

  /v1/payouts/{id}:
    get:
      tags: [Payouts]
      summary: Retrieve a payout
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Payout
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/balance:
    get:
      tags: [Account]
      summary: Retrieve float balance
      responses:
        "200":
          description: Balance
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Balance" }

  /v1/webhook-endpoints:
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        "201":
          description: Endpoint created. The signing secret is returned once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, example: webhook_endpoint }
                  url: { type: string }
                  secret: { type: string }
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      responses:
        "200":
          description: Endpoint list

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Your API key: `Authorization: Bearer sk_sandbox_…`"

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Safely retry a request. Replaying the key with an identical body returns the stored response.
      schema: { type: string }

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Corridor:
      type: object
      properties:
        object: { type: string, example: corridor }
        code: { type: string, example: AE }
        country: { type: string, example: United Arab Emirates }
        source_currency: { type: string, example: USD }
        destination_currency: { type: string, example: AED }
        payout_methods:
          type: array
          items: { type: string, enum: [bank_transfer, mobile_money] }
        min_amount: { type: number }
        max_amount: { type: number }
        estimated_delivery:
          type: object
          properties:
            min_minutes: { type: integer }
            max_minutes: { type: integer }
        routing_depth:
          type: integer
          description: Number of licensed partners the router can choose between.

    Quote:
      type: object
      properties:
        id: { type: string, example: qt_9f1c2b3a4d5e6f70 }
        object: { type: string, example: quote }
        corridor: { type: string, example: AE }
        payout_method: { type: string, example: bank_transfer }
        source_amount: { type: number, example: 25000 }
        source_currency: { type: string, example: USD }
        destination_amount: { type: number, example: 91632.52 }
        destination_currency: { type: string, example: AED }
        rate: { type: number, example: 3.66530064 }
        fee: { type: number, example: 114 }
        total_debit:
          type: number
          description: Amount plus fee. This is what leaves your float.
          example: 25114
        estimated_delivery_minutes: { type: integer, example: 15 }
        status: { type: string, enum: [active, consumed, expired] }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    Beneficiary:
      type: object
      required: [name]
      properties:
        name: { type: string, example: Gulf Star General Trading LLC }
        country: { type: string, example: AE }
        account_number:
          type: string
          description: Required for bank_transfer.
          example: AE070331234567890123456
        bank_name: { type: string, example: Emirates NBD }
        bank_code: { type: string, example: EBILAEAD }
        mobile_number:
          type: string
          description: Required for mobile_money.
          example: "+260971234567"

    Payout:
      type: object
      properties:
        id: { type: string, example: po_1a2b3c4d5e6f7080 }
        object: { type: string, example: payout }
        status:
          type: string
          enum: [received, routed, settled, paid_out, failed]
          description: |
            received  — accepted, screened, float reserved
            routed    — handed to the payout partner
            settled   — funds settled to that partner, float debited
            paid_out  — beneficiary credited in local currency
            failed    — rejected; the reserve is released in full
        reference: { type: string, example: DBK-IMP-8801 }
        quote_id: { type: string }
        corridor: { type: string, example: AE }
        payout_method: { type: string, example: bank_transfer }
        source_amount: { type: number }
        source_currency: { type: string, example: USD }
        fee: { type: number }
        total_debit: { type: number }
        destination_amount: { type: number }
        destination_currency: { type: string }
        rate: { type: number }
        beneficiary: { $ref: "#/components/schemas/Beneficiary" }
        reason_code:
          type: string
          description: Present on failure.
          example: beneficiary_account_invalid
        failure_reason: { type: string }
        metadata: { type: object, additionalProperties: true }
        status_history:
          type: array
          items:
            type: object
            properties:
              status: { type: string }
              detail: { type: string }
              occurred_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Balance:
      type: object
      properties:
        object: { type: string, example: balance }
        currency: { type: string, example: USD }
        balance:
          type: number
          description: Settled position.
        reserved:
          type: number
          description: Committed to payouts in flight.
        available:
          type: number
          description: What the next payout can draw on.
        low_watermark: { type: number }
        as_of: { type: string, format: date-time }

    Event:
      type: object
      description: Webhook payload delivered to your endpoint.
      properties:
        id: { type: string, example: evt_4ceb7e4155a6bd63 }
        object: { type: string, example: event }
        type: { type: string, example: payout.status }
        created_at: { type: string, format: date-time }
        data: { $ref: "#/components/schemas/Payout" }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - idempotency_error
                - api_error
            code: { type: string, example: quote_expired }
            message: { type: string }
            param: { type: string }
