> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sandpay.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a payment

> Simulate a Mobile Money payment for the given operator / country pair.
Pass an explicit `scenario` to deterministically drive the outcome
(handy for testing failure paths); omit it to use the default
`success` path or to defer to a test_client prompt.




## OpenAPI

````yaml /openapi/sandpay.yaml post /v1/payments
openapi: 3.1.0
info:
  title: SandPay API
  version: 0.5.3
  description: |
    Official public API for the SandPay Mobile Money sandbox. Simulate MTN,
    Orange, Moov, and Airtel payment flows across African markets without
    touching real money rails. The API is deterministic, low-latency, and
    designed to mirror the production behaviour of each operator so client
    integrations can be exercised end-to-end before going live.
  contact:
    name: SandPay Support
    url: https://docs.sandpay.dev
    email: support@sandpay.dev
  license:
    name: MIT
    identifier: MIT
servers:
  - url: https://api.sandpay.dev
    description: Production
  - url: https://api.sandbox.sandpay.dev
    description: Sandbox
security:
  - bearerAuth: []
tags:
  - name: Payments
    description: Create, retrieve, list, and refund simulated Mobile Money payments.
  - name: Disbursements
    description: Free-form payouts (merchant → an msisdn), not linked to a collection.
  - name: Meta
    description: API version + capability discovery for keeping integrations current.
  - name: Health
    description: Service health probe.
paths:
  /v1/payments:
    post:
      tags:
        - Payments
      summary: Create a payment
      description: |
        Simulate a Mobile Money payment for the given operator / country pair.
        Pass an explicit `scenario` to deterministically drive the outcome
        (handy for testing failure paths); omit it to use the default
        `success` path or to defer to a test_client prompt.
      operationId: createPayment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentInput'
            examples:
              orangeCI:
                summary: Orange Money Côte d'Ivoire — success
                value:
                  amount: 25000
                  currency: FCFA
                  operator: orange
                  country: CI
                  msisdn: '+22507123456'
                  reference: ORDER-2026-A1
                  scenario: success
      responses:
        '200':
          description: Payment created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '402':
          description: Monthly simulation quota exceeded; upgrade plan to continue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited. Retry after a short delay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    PaymentInput:
      type: object
      required:
        - amount
        - currency
        - operator
        - country
        - msisdn
        - reference
        - application
      properties:
        amount:
          type: integer
          minimum: 1
          example: 25000
        currency:
          type: string
          minLength: 3
          maxLength: 4
          example: FCFA
        operator:
          $ref: '#/components/schemas/PaymentOperator'
        country:
          type: string
          minLength: 2
          maxLength: 2
          example: CI
        msisdn:
          type: string
          pattern: ^\+[1-9][0-9]{6,14}$
          example: '+22507123456'
        reference:
          type: string
          minLength: 1
          maxLength: 80
          example: ORDER-2026-A1
        application:
          type: string
          minLength: 1
          maxLength: 80
          description: >-
            Your store's slug (Settings → Stores). Required — every transaction
            is tied to a store.
          example: my-store
        description:
          type: string
          maxLength: 200
          description: >-
            Optional free-form description (max 200 chars). Stored verbatim on
            the transaction.
          example: Premium plan upgrade
        order_ref:
          type: string
          minLength: 1
          maxLength: 120
          description: >
            Strongly recommended — your order / commande id. Groups this payment

            with its refunds and any recurring charges. Falls back to
            `reference`

            when omitted (B126).
          example: ORDER-2026-A1
        order_url:
          type: string
          format: uri
          maxLength: 500
          description: Optional public URL to the order in your app (B126).
        is_url_protected:
          type: boolean
          description: |
            Set `true` when `order_url` requires a specific app permission to
            open (admin-only, not public). Defaults to `false` (B130).
        scenario:
          allOf:
            - $ref: '#/components/schemas/PaymentScenario'
          default: success
          description: Optional. Defaults to `success`.
    Payment:
      type: object
      required:
        - id
        - type
        - parentTxId
        - orderRef
        - orderUrl
        - isUrlProtected
        - amount
        - commission
        - netAmount
        - customerTotal
        - merchantAbsorptionPct
        - merchantShare
        - customerShare
        - commissionMode
        - currency
        - operator
        - country
        - msisdn
        - reference
        - status
        - latencyMs
        - createdAt
        - raw
      properties:
        id:
          type: string
          example: TX_8K3M9F
          description: Transaction id.
        type:
          $ref: '#/components/schemas/PaymentType'
        parentTxId:
          type: string
          nullable: true
          description: |
            For a `refund`, the id of the original collection it reverses.
            `null` for collections and disbursements (B119).
          example: null
        orderRef:
          type: string
          description: >
            App order id grouping related transactions (collection + refunds +

            recurring charges). Falls back to `reference` when not supplied
            (B126).
          example: ORDER-2026-A1
        orderUrl:
          type: string
          format: uri
          nullable: true
          description: >-
            Public URL to the order in the consuming app; `null` when not set
            (B126).
          example: null
        isUrlProtected:
          type: boolean
          description: |
            Whether `orderUrl` requires a specific permission on the consuming
            app to open (admin-only, not public). The dashboard shows a
            "protected URL" hint accordingly. Defaults to `false` (B130).
          example: false
        amount:
          type: integer
          minimum: 1
          description: |
            Gross amount in minor units the customer was debited (e.g. CFA
            centimes are not used; FCFA uses whole units).
          example: 25000
        commission:
          type: number
          minimum: 0
          description: |
            Operator commission kept from the gross `amount` (B112). Defaults
            to 1% (1.5% for Moov). `0` when no settlement / commission applies.
          example: 250
        netAmount:
          type: number
          minimum: 0
          description: |
            Net the merchant received. Reconcile your merchant ledger against
            this, not `amount`. In `customer` mode this equals `amount` (the
            merchant gets the full amount); in `merchant` mode it is
            `amount − commission`.
          example: 25000
        customerTotal:
          type: number
          minimum: 0
          description: |
            Total the client was charged (`netAmount + commission`). In
            `customer` mode this is `amount + commission`; in `merchant` mode
            it equals `amount` (B113).
          example: 25250
        merchantAbsorptionPct:
          type: integer
          nullable: true
          minimum: 0
          maximum: 100
          description: |
            Percentage (0–100) of the operator commission the merchant absorbed
            for this payment (B114). `100` = merchant absorbs the full fee
            (default); `0` = customer absorbs all. `null` for legacy rows.
          example: 100
        merchantShare:
          type: number
          minimum: 0
          description: |
            Portion of the commission borne by the merchant
            (`amount − netAmount`). B114.
          example: 250
        customerShare:
          type: number
          minimum: 0
          description: |
            Portion of the commission borne by the customer
            (`commission − merchantShare`). B114.
          example: 0
        commissionMode:
          type: string
          nullable: true
          enum:
            - customer
            - merchant
          description: |
            Who absorbed the operator commission for this payment (B113):
            `customer` (default) — client pays `amount + commission`, merchant
            receives the full `amount`; `merchant` — client pays `amount`,
            merchant receives `amount − commission`. `null` for legacy rows.
          example: customer
        currency:
          type: string
          minLength: 3
          maxLength: 4
          example: FCFA
          description: ISO-4217-style currency code (`FCFA`, `XOF`, `XAF`).
        operator:
          $ref: '#/components/schemas/PaymentOperator'
        country:
          type: string
          minLength: 2
          maxLength: 2
          example: CI
          description: ISO-3166 2-letter country code.
        msisdn:
          type: string
          pattern: ^\+[1-9][0-9]{6,14}$
          example: '+22507123456'
          description: E.164-formatted phone number.
        reference:
          type: string
          minLength: 1
          maxLength: 80
          example: ZANA-PAY-20260531221015-8E2884A47B1C
          description: >-
            Caller-supplied idempotency reference (unique per org). Recommended
            canonical shape `ORIGIN-OP-TS-CODE` (e.g.
            `ZANA-PAY-20260531221015-8E2884A47B1C`): ORIGIN = your app slug
            (`SP` for SandPay), OP ∈ PAY/REF/DIS/ABO, TS = compact UTC datetime
            `YYYYMMDDHHMMSS`, CODE = 12-char uppercase hex. Any string is
            accepted — this is a recommended convention, not enforced. Distinct
            from `order_ref`.
        scenario:
          allOf:
            - $ref: '#/components/schemas/PaymentScenario'
          nullable: true
          description: Scenario used to drive the outcome, if explicit.
        status:
          $ref: '#/components/schemas/PaymentStatus'
        latencyMs:
          type: integer
          minimum: 0
          description: Total time spent processing the payment, in milliseconds.
          example: 1240
        createdAt:
          type: string
          format: date-time
          description: ISO-8601 timestamp when the payment was created.
        raw:
          type: object
          nullable: true
          additionalProperties: true
          description: |
            Native operator response payload. In sandbox this is *synthesized*
            to mirror the real operator shape (MTN MoMo, Orange Money, Moov
            Africa, Airtel Money) — with `_simulated: true` set on the
            top level so consumers can detect sandbox payloads. In production
            this is the verbatim payload returned by the upstream operator
            API.

            This field lets you write integration code (e.g.
            `tx.raw.financialTransactionId` for MTN, `tx.raw.responseCode`
            for Moov) against sandbox transactions and have the same code
            work in production without changes.

            `null` for legacy rows persisted before raw passthrough shipped.
          example:
            _simulated: true
            amount: '25000'
            currency: FCFA
            externalId: ORDER-2026-A1
            payer:
              partyIdType: MSISDN
              partyId: '22507123456'
            payerMessage: Payment
            payeeNote: ORDER-2026-A1
            status: SUCCESSFUL
            financialTransactionId: SIM_A1B2C3D4
    ErrorResponse:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: >-
            Machine-readable error code (e.g. `invalid_body`, `unauthorized`,
            `quota_exceeded`).
          example: quota_exceeded
        message:
          type: string
          description: Human-readable description of what went wrong.
          example: Monthly simulation quota exceeded.
        detail:
          type: object
          nullable: true
          description: Optional structured detail, e.g. field validation errors.
          additionalProperties: true
    PaymentOperator:
      type: string
      enum:
        - mtn
        - orange
        - moov
        - airtel
      description: Mobile Money operator code.
    PaymentScenario:
      type: string
      enum:
        - success
        - pin_invalid
        - low_balance
        - timeout
        - blocked
        - cancelled
        - unknown_msisdn
        - limit_exceeded
        - maintenance
        - duplicate
      description: Scenario tag that drives the simulated outcome.
    PaymentType:
      type: string
      enum:
        - collection
        - refund
        - disbursement
      description: |
        Transaction direction (B119): `collection` (pay-in, customer →
        merchant), `refund` (payout reversing a collection), or
        `disbursement` (free-form payout, merchant → an msisdn).
    PaymentStatus:
      type: string
      enum:
        - SUCCESS
        - PIN_INVALID
        - INSUFFICIENT_FUNDS
        - TIMEOUT
        - ACCOUNT_BLOCKED
        - USER_CANCELLED
        - UNKNOWN_MSISDN
        - LIMIT_EXCEEDED
        - SERVICE_UNAVAILABLE
        - DUPLICATE_REFERENCE
        - PENDING
      description: Final settlement status.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key in format `sp_sk_test_...` or `sp_sk_live_...`.

````