openapi: 3.1.0
info:
  title: AbePay Cashier Integration API
  version: "1.0.0"
  summary: List AbePay as a payment method inside a broker's own cashier.
  description: |
    AbePay is a payment gateway between a broker and its clients in Africa. It appears
    as a method inside the broker's cashier: the client taps Deposit, picks AbePay, pays
    from the mobile wallet, bank account or stablecoin they already hold, and their
    balance moves the moment funds are verified. Withdrawals run the same way in reverse.

    The client never leaves the broker's site, never creates an AbePay account, and never
    re-verifies their identity. AbePay performs no client KYC. The broker identifies the client,
    and every payment instrument is verified by its issuer before it can transact.

    ### Two rules govern both directions

    1. A rail confirmation is never proof of payment. AbePay re-verifies every payment
       independently before notifying the broker of anything.
    2. A payout is only ever instructed after the broker has debited its own client, so a
       disbursement cannot exist without a matching debit.

    ### Deposit sequence

    Broker opens a session → client pays on the AbePay page → rail confirmation →
    AbePay re-verifies independently → `deposit.completed` webhook → broker credits.
    Do not credit on the return_url. A client returning to the site is a UI event, not a
    payment confirmation.

    ### Payout sequence

    Broker verifies and debits its client → `POST /payouts` → AbePay disburses →
    AbePay verifies → `payout.completed` webhook. A `202` is an acceptance, not a
    completion.

    ### Settlement

    Client balances move instantly on the webhook; funds between AbePay and the broker
    settle net and periodically against a daily reconciliation file.

    ### Coverage

    Live in Kenya (KES), Uganda (UGX), Tanzania (TZS), Nigeria (NGN) and South Africa (ZAR)
    in both directions, with stablecoin (USDT, USDC) available in every market. Adding a
    market is a configuration change on AbePay's side, not an integration change on the
    broker's.

    We are willing to implement against the broker's own cashier specification instead of
    this one. Narrative documentation: https://app.abepayy.com/developers

  contact:
    name: AbePay Partnerships
    email: support@abepayy.com
    url: https://app.abepayy.com/solution
  license:
    name: Proprietary. Use governed by the AbePay integration agreement.

servers:
  - url: https://api.abepayy.com
    description: Production
  - url: https://sandbox-api.abepayy.com
    description: Sandbox. Full flow, no real money; amounts drive the outcome.

tags:
  - name: Methods
    description: What a given client can actually pay with.
  - name: Deposits
    description: Money in, from the client to the broker.
  - name: Payouts
    description: Money out, from the broker to the client.
  - name: Reporting
    description: Transaction lookup and settlement.

security:
  - signedRequest: []

paths:
  /cashier/v1/methods:
    get:
      tags: [Methods]
      operationId: listMethods
      summary: Methods available to this client
      description: |
        Called when the cashier renders. Returns what this client can actually use, with
        live limits, the rate they would get, and what to ask a withdrawing client for.

        A market where AbePay holds no local rail connectivity returns stablecoin only, so the
        method never disappears entirely. Availability is live: a degraded rail is omitted rather
        than offered and then failed.
      parameters:
        - name: country
          in: query
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
          description: ISO 3166-1 alpha-2 country of the client, as held by the broker.
          example: KE
        - name: currency
          in: query
          required: true
          schema: { type: string, minLength: 3, maxLength: 3 }
          description: The currency of the client's trading account.
          example: USD
      responses:
        "200":
          description: Available methods.
          content:
            application/json:
              schema:
                type: object
                properties:
                  methods:
                    type: array
                    items: { $ref: "#/components/schemas/Method" }
              example:
                methods:
                  - code: mpesa_ke
                    label: M-Pesa
                    currency: KES
                    directions: [deposit, payout]
                    min_usd: 1
                    max_usd: 2000
                    rate: { deposit: 130.0, payout: 124.0 }
                    eta_seconds: { deposit: 45, payout: 90 }
                    destination_field: { type: msisdn, pattern: "^254[0-9]{9}$" }
                  - code: usdt_trc20
                    label: USDT (TRC-20)
                    currency: USD
                    directions: [deposit, payout]
                    min_usd: 5
                    max_usd: 50000
                    rate: { deposit: 1.0, payout: 1.0 }
                    eta_seconds: { deposit: 120, payout: 60 }
                    destination_field: { type: address, pattern: "^T[A-Za-z0-9]{33}$" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /cashier/v1/deposits:
    post:
      tags: [Deposits]
      operationId: createDeposit
      summary: Open a deposit session
      description: |
        Creates a deposit session and returns the URL the cashier opens, in an iframe, a modal
        or a redirect. The client completes payment there; AbePay notifies the broker
        by `deposit.completed` webhook once funds are confirmed and independently verified.

        Credit the client on the webhook only, never on `return_url`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reference, client_ref, country, return_url]
              properties:
                reference:
                  type: string
                  description: The broker's identifier for this deposit. Echoed on every webhook and lookup.
                  example: DRV-DEP-91847362
                client_ref:
                  $ref: "#/components/schemas/ClientRef"
                country:
                  type: string
                  minLength: 2
                  maxLength: 2
                  description: ISO 3166-1 alpha-2. Determines which methods are offered.
                  example: KE
                amount_usd:
                  type: number
                  minimum: 1
                  description: Amount the client wants credited. Omit to let the client choose in the AbePay interface.
                  example: 100
                method:
                  type: string
                  description: Pre-select a method code from /methods. Omit to show the client the full list.
                  example: mpesa_ke
                return_url:
                  type: string
                  format: uri
                  description: Where to send the client when the flow ends, successfully or not.
                  example: https://cashier.deriv.com/return/91847362
      responses:
        "201":
          description: Session created. Open `payment_url` for the client.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transaction_id: { $ref: "#/components/schemas/TransactionId" }
                  reference: { type: string, example: DRV-DEP-91847362 }
                  status: { type: string, const: pending }
                  payment_url:
                    type: string
                    format: uri
                    example: https://pay.abepayy.com/s/01J8XK4M2Q
                  expires_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/BusinessRule" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ProviderDegraded" }

  /cashier/v1/payouts:
    post:
      tags: [Payouts]
      operationId: createPayout
      summary: Instruct a payout
      description: |
        Call after the broker has verified its client and debited their balance. AbePay
        accepts the instruction, disburses over the rail, verifies
        the payout independently, and confirms by `payout.completed`.

        A 202 is an acceptance, not a completion. Treat the payout as in flight until the webhook
        arrives. On `payout.failed`, re-credit the client.

        A timeout is not a failure. Look the transaction up by reference before retrying. This is
        the most common cause of double payouts in any integration.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reference, idempotency_key, client_ref, amount_usd, method, destination]
              properties:
                reference:
                  type: string
                  description: The broker's identifier for this payout.
                  example: DRV-WDR-55120934
                idempotency_key:
                  type: string
                  description: |
                    A retried instruction with the same key pays exactly once and returns the
                    original result, including while the first is still in flight. Reusing a
                    key with a different payload is a 409.
                  example: DRV-WDR-55120934
                client_ref:
                  $ref: "#/components/schemas/ClientRef"
                amount_usd:
                  type: number
                  minimum: 1
                  description: Amount debited from the client, in USD.
                  example: 100
                method:
                  type: string
                  description: A method code from /methods that supports the payout direction.
                  example: mpesa_ke
                destination:
                  type: string
                  description: |
                    The value collected from the client, matching that method's
                    `destination_field.pattern`: a mobile number, bank account or chain address.
                  example: "254712345678"
      responses:
        "202":
          description: Accepted and dispatched. Await `payout.completed`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transaction_id: { $ref: "#/components/schemas/TransactionId" }
                  reference: { type: string, example: DRV-WDR-55120934 }
                  status: { type: string, const: processing }
                  amount_local: { type: number, example: 12400 }
                  currency: { type: string, example: KES }
                  rate: { type: number, example: 124.0 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/BusinessRule" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ProviderDegraded" }

  /cashier/v1/transactions/{id}:
    get:
      tags: [Reporting]
      operationId: getTransaction
      summary: Authoritative status for one transaction
      description: |
        Use for reconciliation, for a support query, and any time a webhook did not arrive.
        A terminal status here is final regardless of what a later webhook retry appears to
        say. Never infer a failure from a timeout; look it up.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: The AbePay transaction id, or the broker's own reference.
          example: abe_dep_01J8XK4M2Q
      responses:
        "200":
          description: The transaction.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Transaction" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: No such transaction.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /cashier/v1/settlements:
    get:
      tags: [Reporting]
      operationId: getSettlement
      summary: Daily settlement and reconciliation
      description: |
        Client balances move instantly on the webhook; funds between AbePay and the broker
        move net and periodically. This returns the gross in each direction, the net position
        for the cycle, and a link to the per-transaction file with every rail reference.

        Cadence, whether daily, weekly or on threshold, is a commercial term rather than a technical one.
      parameters:
        - name: date
          in: query
          required: true
          schema: { type: string, format: date }
          example: "2026-08-26"
      responses:
        "200":
          description: Settlement summary for the day.
          content:
            application/json:
              schema:
                type: object
                properties:
                  date: { type: string, format: date }
                  currency: { type: string, example: USD }
                  deposits:
                    type: object
                    properties:
                      count: { type: integer, example: 1284 }
                      gross: { type: number, example: 48210.00 }
                  payouts:
                    type: object
                    properties:
                      count: { type: integer, example: 902 }
                      gross: { type: number, example: 39115.00 }
                  net_due_to_broker: { type: number, example: 9095.00 }
                  status:
                    type: string
                    enum: [open, settled]
                  transactions_url: { type: string, format: uri }
        "401": { $ref: "#/components/responses/Unauthorized" }

webhooks:
  depositCompleted:
    post:
      summary: deposit.completed. Credit the client now
      description: |
        Funds confirmed and independently verified. This is the only signal on which the broker
        should credit.

        Webhooks may arrive more than once. A retry after a lost acknowledgement is
        indistinguishable from a first delivery. Key on `transaction_id` and make handling
        idempotent. Respond 2xx to acknowledge; anything else, or no response within 10
        seconds, is retried with exponential backoff for 24 hours.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      responses:
        "200": { description: Acknowledged. }

  depositFailed:
    post:
      summary: deposit.failed. Credit nothing
      description: The client did not pay, the payment was reversed, or the session expired.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      responses:
        "200": { description: Acknowledged. }

  payoutCompleted:
    post:
      summary: payout.completed. The debit stands
      description: The client has the money, independently verified.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      responses:
        "200": { description: Acknowledged. }

  payoutFailed:
    post:
      summary: payout.failed. Re-credit the client
      description: The disbursement did not succeed. The reason is in the payload.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      responses:
        "200": { description: Acknowledged. }

components:
  securitySchemes:
    signedRequest:
      type: apiKey
      in: header
      name: X-Abe-Signature
      description: |
        Every request in both directions is signed:

            X-Abe-Key        your API key identifier
            X-Abe-Timestamp  unix seconds; older than 300s is rejected
            X-Abe-Nonce      unique per request; a repeat within the window is rejected
            X-Abe-Signature  hex(HMAC-SHA256(secret, timestamp + "." + nonce + "." + rawBody))

        Compare in constant time. Mutual TLS and IP allow-listing are available in addition.
        Keys are per-environment and rotatable without downtime; both secrets verify during
        the rotation window.

        The most common cause of a 401 is clock skew. Check that first.

  schemas:
    ClientRef:
      type: string
      description: |
        An opaque, stable reference for the client, issued by the broker. AbePay never
        resolves it to a person and performs no KYC. Do not send names, documents or dates
        of birth; they are rejected.
      example: c_9f3a20b1

    TransactionId:
      type: string
      description: AbePay's identifier. Idempotency and webhook de-duplication key on this.
      example: abe_dep_01J8XK4M2Q

    Status:
      type: string
      enum: [pending, processing, completed, failed, expired]
      description: |
        pending: deposit session open, awaiting the client. No funds have moved.
        processing: funds are moving.
        completed: terminal, verified and final.
        failed: terminal. No funds moved, or a reversal completed. Safe to re-credit.
        expired: terminal. A session the client never completed.

        A transaction only moves forward and never leaves a terminal state.

    Method:
      type: object
      properties:
        code: { type: string, example: mpesa_ke }
        label:
          type: string
          description: Display name for the cashier.
          example: M-Pesa
        currency: { type: string, example: KES }
        directions:
          type: array
          items: { type: string, enum: [deposit, payout] }
        min_usd: { type: number, example: 1 }
        max_usd: { type: number, example: 2000 }
        rate:
          type: object
          properties:
            deposit: { type: number }
            payout: { type: number }
        eta_seconds:
          type: object
          description: Typical end-to-end time, for display in the cashier.
          properties:
            deposit: { type: integer }
            payout: { type: integer }
        destination_field:
          type: object
          description: |
            What to ask a withdrawing client for, and how to validate it client-side. Differs
            per method, so the cashier need not hard-code any of them.
          properties:
            type:
              type: string
              enum: [msisdn, bank_account, address]
            pattern:
              type: string
              description: Regular expression the value must match.

    Transaction:
      type: object
      properties:
        transaction_id: { $ref: "#/components/schemas/TransactionId" }
        reference: { type: string, example: DRV-DEP-91847362 }
        type:
          type: string
          enum: [deposit, payout]
        status: { $ref: "#/components/schemas/Status" }
        client_ref: { $ref: "#/components/schemas/ClientRef" }
        amount_usd: { type: number, example: 100 }
        amount_local: { type: number, example: 13000 }
        currency: { type: string, example: KES }
        rate: { type: number, example: 130.0 }
        method: { type: string, example: mpesa_ke }
        provider_reference:
          type: string
          description: The rail reference, or the on-chain transaction hash.
          example: SGH4XY9Z12
        failure_reason:
          type: string
          description: Present on failed transactions. Phrased for display to an end client.
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    WebhookEnvelope:
      type: object
      required: [event, sent_at, data]
      properties:
        event:
          type: string
          enum: [deposit.completed, deposit.failed, payout.completed, payout.failed]
        sent_at: { type: string, format: date-time }
        data: { $ref: "#/components/schemas/Transaction" }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable, stable.
              example: amount_below_minimum
            message:
              type: string
              description: Already phrased for an end client, safe to display without translation.
              example: The minimum deposit for this method is $1.
            retryable:
              type: boolean
              description: Whether an identical retry could succeed.

  responses:
    Unauthorized:
      description: Signature, timestamp or nonce rejected. Check clock skew first.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BusinessRule:
      description: |
        Valid request, refused on business rules: outside limits, unsupported method for that
        country, or a screened payout destination. Show the message to the client.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limited. Back off and retry with jitter.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ProviderDegraded:
      description: |
        A rail is degraded and AbePay is failing closed rather than accepting money it cannot
        settle. Retry, or offer another method. /methods reflects availability live.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
