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

# Simulate Transaction Outcome

> Sandbox only; returns 403 in production. Completes a pending sandbox deposit or payout with the outcome you choose, and your balance and transaction webhook behave as they would in production. The transaction must still be `PENDING`, `PROCESSING` or `UNKNOWN`: a deposit waiting for an OTP must be authorized first. Use it for a payment method whose account number, phone number or card ends in `0003`, which stays pending until simulated, or to complete any other pending sandbox transaction early. The result arrives by webhook shortly after this call returns.

<Note>
  **Sandbox only.** Returns `403` in production.
</Note>

Completes a pending sandbox transaction with the outcome you choose. Your balance and the `TRANSACTION.UPDATED` webhook behave exactly as they would in production.

Use this whenever you want a deterministic outcome without waiting on the default sandbox settlement window (\~5 minutes):

* **Pending on purpose.** A payment method whose account number, phone number or card ends in `0003` stays `PENDING` until you call this endpoint.
* **Speed up any other pending sandbox transaction.** Call it to finalize sooner than the \~5 minute default.

The transaction must still be `PENDING`, `PROCESSING` or `UNKNOWN`. A deposit waiting on OTP (`CUSTOMER_ACTION_REQUIRED`) must be authorized via [Authorize Transaction](/api-reference/endpoint/transactions/authorize) first.

See the [Sandbox testing guide](/guides/testing-in-sandbox) for the full test-value taxonomy.


## OpenAPI

````yaml POST /api/v1/transaction/{transactionId}/simulate
openapi: 3.1.0
info:
  title: Afriex Business API
  version: 1.0.13
  description: >-
    Welcome to the Afriex Business API. This API allows you to manage customers,

    process payments, handle payouts, and receive real-time notifications via

    webhooks.


    For detailed guidance on authentication, pagination, error handling, and

    webhooks, please refer to the [dedicated

    guides](https://docs.afriex.com) in the top bar. The guide

    provides a step-by-step instructions to help you integrate seamlessly.


    ## Testing in sandbox


    Send requests to `https://sandbox.api.afriex.com` with a sandbox API key.

    No real money moves. Payouts, mobile money and card deposits, and virtual

    accounts can be tested in every currency your business supports.


    **When transactions complete.** A sandbox deposit or payout completes on

    its own about 5 minutes after it is created, and your webhook receives the

    final status, exactly as in production. To complete one sooner:


    - Include `SIMULATE_INSTANT` anywhere in `meta.reference` when you create
      it, and it completes in about 30 seconds.
    - Or call `POST /api/v1/transaction/{transactionId}/simulate` with
      `{"outcome": "success"}` or `{"outcome": "failed"}` to complete it now.

    **Choosing an outcome.** The last four digits of the bank account number,

    the mobile money phone number or the card number decide what happens:


    | Ends in | Payout | Mobile money deposit | Card deposit |

    | --- | --- | --- | --- |

    | anything else | Succeeds | Succeeds | Succeeds |

    | `0001` | Fails; your balance is refunded | Fails | Fails |

    | `0002` | Succeeds | Waits for a one-time password (see below) | Succeeds |

    | `0003` | Stays pending until you call `/simulate` | Stays pending until
    you call `/simulate` | Stays pending until you call `/simulate` |

    | `0004` | Account name lookup (`GET /api/v1/payment-method/resolve`) fails
    | Succeeds | Succeeds |

    | `0005` | Declined immediately | Declined immediately | Declined
    immediately |


    A phone number must still be a valid mobile number for its country, so keep

    the country prefix and change only the last digits, for example

    `+254712340002` in Kenya. Account name lookups return `Sandbox Test
    Account`.


    **One-time passwords.** A mobile money deposit from a wallet ending in

    `0002` returns `CUSTOMER_ACTION_REQUIRED`. Complete it with

    `POST /api/v1/transaction/{transactionId}/authorize` and the OTP `123456`;

    any other OTP returns `OTP_INCORRECT`. The deposit then completes as above.


    **Virtual accounts.** Sandbox virtual accounts receive no money on their

    own. Pay into one with

    `POST /api/v1/payment-method/virtual-account/simulate-transfer`.


    **Cards.** Collect the card as you would in production, using a test card

    number whose last four digits choose the outcome.


    **Balances and webhooks.** Fund your sandbox balance with

    `POST /api/v1/org/balance/topup`, and send yourself a sample webhook with

    `POST /api/v1/webhooks/trigger`.
  termsOfService: https://www.afriex.com/terms-and-condition
  contact:
    name: Afriex API Support
    email: support@afriex.com
    url: https://docs.afriex.com
  license:
    name: Proprietary
    url: https://www.afriex.com/terms-and-condition
servers:
  - url: https://sandbox.api.afriex.com
    description: Staging Base URL
  - url: https://api.afriex.com
    description: Production Base URL
security:
  - ApiKey: []
tags:
  - name: Customers
    description: Create and manage your customers.
  - name: Payment Methods
    description: Register and resolve customer payout and collection methods.
  - name: Transactions
    description: Create and track deposits, withdrawals, and swaps.
  - name: Balance
    description: View and top up your business wallet balances.
  - name: Rates
    description: Fetch real-time exchange rates.
  - name: Checkout Sessions
    description: Create hosted checkout sessions.
  - name: Webhooks
    description: Webhook event payloads and sandbox webhook testing.
  - name: Media
    description: Generate presigned URLs for secure file uploads.
  - name: Payment Batches
    description: >-
      Manage payment batches, their recipients and their runs.


      Where payload signing is enabled for your business, sign the JSON of the
      request body for POST and PATCH routes that take one. For GET routes with
      `page` and `limit`, and for the withdraw route, sign the query parameters
      as the server reads them, with defaults applied: a list call with no
      parameters signs `{"limit":100,"page":0}` and a withdraw with no
      `sessionId` signs `{}`. For the remaining routes (get, delete and
      remove-recipient) sign the path parameters, e.g. `{"batchId":"..."}`.
paths:
  /api/v1/transaction/{transactionId}/simulate:
    parameters:
      - $ref: '#/components/parameters/x-api-version'
    post:
      tags:
        - Transactions
      summary: Simulate a sandbox transaction outcome
      description: >-
        Sandbox only; returns 403 in production. Completes a pending sandbox
        deposit or payout with the outcome you choose, and your balance and
        transaction webhook behave as they would in production. The transaction
        must still be `PENDING`, `PROCESSING` or `UNKNOWN`: a deposit waiting
        for an OTP must be authorized first. Use it for a payment method whose
        account number, phone number or card ends in `0003`, which stays pending
        until simulated, or to complete any other pending sandbox transaction
        early. The result arrives by webhook shortly after this call returns.
      operationId: simulateTransaction
      parameters:
        - name: transactionId
          in: path
          description: The unique identifier of the transaction
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                outcome:
                  type: string
                  description: The status to finalize the transaction to.
                  enum:
                    - success
                    - failed
              required:
                - outcome
            examples:
              success:
                summary: Complete a pending sandbox deposit
                value:
                  outcome: success
      responses:
        '202':
          description: Accepted. The transaction as it stands before it is finalized.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Transaction'
        '400':
          description: >-
            The transaction cannot be simulated, is waiting for authorization,
            or is already completed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not available in production.
        '404':
          description: Transaction not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  parameters:
    x-api-version:
      name: x-api-version
      in: header
      required: false
      description: >-
        API version in ISO 8601 format. The only supported version is
        `2026-05-18`, which is also the default when the header is omitted. Any
        other value is rejected with a `400 Bad Request`.
      schema:
        type: string
  schemas:
    Transaction:
      type: object
      properties:
        transactionId:
          type: string
          description: The unique identifier for the transaction.
        customerId:
          type: string
          description: The unique identifier of the customer.
        sourceId:
          type: string
          description: The unique identifier of the source payment method.
        destinationId:
          type: string
          description: The unique identifier of the destination payment method.
        sourceAmount:
          type: string
          description: The souce transaction amount value
        sourceCurrency:
          type: string
          description: The currency code of the wallet charged.
        destinationAmount:
          type: string
          description: The destination transaction amount value
        destinationCurrency:
          type: string
          description: The description currency code.
        type:
          type: string
          enum:
            - DEPOSIT
            - WITHDRAW
            - SWAP
          description: The type of transaction.
        channel:
          type: string
          enum:
            - BANK_ACCOUNT
            - SWIFT
            - MOBILE_MONEY
            - UPI
            - INTERAC
            - WE_CHAT
            - CARD
            - CRYPTO
            - VIRTUAL_BANK_ACCOUNT
            - POOL_ACCOUNT
            - ACH_BANK_ACCOUNT
            - PAYBILL_TILL
            - RFP
            - VIRTUAL_CARD
            - ALIPAY
            - WALLET
            - PAYMENT_LINQ
            - ADMIN
          description: >-
            The payment channel of the transaction. `ADMIN` marks an adjustment
            Afriex made to your wallet; `PAYMENT_LINQ` a payment made through a
            payment link.
        status:
          type: string
          enum:
            - PENDING
            - PROCESSING
            - SUCCESS
            - FAILED
            - CANCELLED
            - REFUNDED
            - RETRY
            - UNKNOWN
            - SCHEDULED
            - CUSTOMER_ACTION_REQUIRED
            - REJECTED
            - IN_REVIEW
            - RFI_REQUESTED
            - DISPUTED
            - DISPUTE_RESOLVED
            - DISPUTE_WON
            - DISPUTE_LOST
            - DISPUTE_EVIDENCE_SUBMITTED
          description: >-
            The current status of the transaction.


            `IN_REVIEW` and `RFI_REQUESTED` are review states: the transaction
            is still in flight and is waiting on a review, not on you. Treat
            them as non-terminal and keep polling or listening for
            `TRANSACTION.UPDATED`. `RFI_REQUESTED` may result in someone
            contacting you for more information about the transfer.
        merchantReference:
          type: string
          description: >-
            The merchant-supplied reference for the transaction (mirrors
            meta.reference from the create request).
        rate:
          type: string
          description: >-
            The realized source-to-destination exchange rate for the
            transaction, expressed as `1 sourceCurrency = rate
            destinationCurrency` (equal to destinationAmount / sourceAmount).
        fee:
          type: string
          description: >-
            The fee charged for this transaction, denominated in
            `sourceCurrency`. It is reported separately from `sourceAmount`.
            Omitted when no fee applied to the transaction.
        meta:
          type: object
          description: >-
            Transaction metadata. Echoes the metadata you attached on create and
            may include server-set state flags such as `otpRequired` and
            `failureReason`.
          properties:
            narration:
              type: string
              description: >-
                The narration you attached when creating the transaction, echoed
                back. An empty string when none was provided.
            otpRequired:
              type: boolean
              description: >-
                Returned on deposits that may need an extra authorization step.
                When `true`, the deposit is waiting for the customer to submit a
                one-time password; call `POST
                /transaction/{transactionId}/authorize` to complete it.
            failureReason:
              type: object
              description: >-
                Present only when `status` is `FAILED` or `REJECTED`. Carries a
                stable `AFX_*` code and a customer-safe message. Branch on
                `code` rather than the underlying rail so your integration stays
                stable across routing changes.
              required:
                - code
                - message
                - retryable
              properties:
                code:
                  type: string
                  description: >-
                    Stable `AFX_*` failure code. Safe to switch on; the set
                    grows over time but existing values do not change meaning.
                  example: AFX_VELOCITY_LIMIT_EXCEEDED
                  enum:
                    - AFX_REQUEST_FAILED
                    - AFX_SYSTEM_ERROR
                    - AFX_SERVICE_UNAVAILABLE
                    - AFX_INVALID_CURRENCY
                    - AFX_INVALID_AMOUNT
                    - AFX_INVALID_RECIPIENT
                    - AFX_RECIPIENT_NOT_FOUND
                    - AFX_ACCOUNT_CLOSED
                    - AFX_NAME_MISMATCH
                    - AFX_BENEFICIARY_RESTRICTED
                    - AFX_INVALID_SENDER
                    - AFX_INVALID_REQUEST
                    - AFX_VELOCITY_LIMIT_EXCEEDED
                    - AFX_AMOUNT_LIMIT_EXCEEDED
                    - AFX_PAYMENT_FAILED
                    - AFX_COMPLIANCE_REJECTED
                    - AFX_PROPOSAL_EXPIRED
                message:
                  type: string
                  description: >-
                    Customer-safe short description of the failure. Suitable for
                    display; do not parse — branch on `code` instead.
                retryable:
                  type: boolean
                  description: >-
                    `true` when re-submitting the same request (or, for
                    `AFX_PROPOSAL_EXPIRED`, starting a fresh proposal) may
                    succeed. `false` when the caller must change the request
                    before retrying.
          additionalProperties: true
        createdAt:
          type: string
          description: The date and time the transaction was created.
        updatedAt:
          type: string
          description: The date and time the transaction was last updated.
    ErrorResponse:
      type: object
      properties:
        code:
          type: string
          description: Machine-readable error code.
        error:
          type: string
          description: Human-readable error message.
        details:
          $ref: '#/components/schemas/ErrorDetails'
    ErrorDetails:
      type: object
      properties:
        errorMessage:
          type: string
          description: Detailed/technical error message.
        friendlyMessage:
          type: string
          description: User-facing error message safe to display.
        data:
          type: object
          description: >-
            Optional caller-safe context for the error. On a customer-create
            uniqueness conflict (EMAIL_ALREADY_EXISTS /
            PHONE_NUMBER_ALREADY_EXISTS) this carries the existing customer's
            id, so you can adopt it without a follow-up lookup.
          properties:
            customerId:
              type: string
              description: Id of the existing customer (on a create conflict).
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Static business API key issued from the dashboard. A business can

        provision **multiple API keys**, each scoped to a configurable set of

        **permissions** (e.g. read transactions, create deposits, etc).
        Permissions are chosen per key at creation time in the

        dashboard and may be revoked by deleting the key. Requests made with a

        key that does not include the permission required by the target

        endpoint is rejected with a `401 Unauthorized` response, the same

        response an unrecognised, malformed, revoked or disabled key returns.
        The API

        does not distinguish the two cases on the wire.

        Manage your keys and their permissions under **Developer → API keys**

        in the dashboard.


        The permission each endpoint requires is below. Where several are

        listed, any one of them is enough. Keys created before permissions

        existed carry none and keep access to every endpoint.


        | Endpoint | Permission |

        | --- | --- |

        | `GET /customer`, `GET /customer/{customerId}` | `CUSTOMER.READ` |

        | `POST /customer` | `CUSTOMER.CREATE` |

        | `PATCH /customer/{customerId}`, `PATCH /customer/{customerId}/kyc`,
        `POST /customer/{customerId}/verify` | `CUSTOMER.UPDATE` |

        | `DELETE /customer/{customerId}` | `CUSTOMER.DELETE` |

        | `GET /payment-method`, `GET /payment-method/{paymentMethodId}`, `GET
        /payment-method/institution`, `GET /payment-method/institution/codes`,
        `GET /payment-method/resolve`, `GET /payment-method/virtual-account`,
        `GET /payment-method/pool-account` | `PAYMENT_METHOD.READ` |

        | `POST /payment-method`, `DELETE /payment-method/{paymentMethodId}`,
        `POST /payment-method/virtual-account`, `GET
        /payment-method/crypto-wallet`, `POST
        /payment-method/virtual-account/simulate-transfer` (sandbox only) |
        `PAYMENT_METHOD.CREATE` |

        | `POST /transaction` | `TRANSACTION.DEPOSIT.CREATE`,
        `TRANSACTION.WITHDRAW.CREATE` or `TRANSACTION.SWAP.CREATE` |

        | `POST /transaction/{transactionId}/authorize`, `POST
        /transaction/pool-account` | `TRANSACTION.DEPOSIT.CREATE` |

        | `POST /transaction/{transactionId}/simulate` (sandbox only) |
        `TRANSACTION.DEPOSIT.CREATE` or `TRANSACTION.WITHDRAW.CREATE` |

        | `GET /transaction`, `GET /transaction/{transactionId}`, `GET
        /transaction/{transactionId}/advice` | `TRANSACTION.HISTORY.READ` |

        | `GET /org/balance`, `GET /org/rates` | `WALLET.BALANCES.READ` |

        | `POST /media/url` | `PAYMENT_METHOD.CREATE` |

        | `POST /sme-registration` | `COMPLIANCE.KYB.SUBMIT` |

        | `GET /sme-registration/status` | `COMPLIANCE.KYB.READ` |

        | `POST /checkout-session` | `CHECKOUT_LINK.CREATE` |

        | `GET /payment-batch`, `GET /payment-batch/{batchId}`, `GET
        /payment-batch/{batchId}/recipients` | `PAYMENT_METHOD.READ` |

        | `POST /payment-batch`, `PATCH /payment-batch/{batchId}`, `DELETE
        /payment-batch/{batchId}`, `POST /payment-batch/{batchId}/recipients`,
        `POST /payment-batch/{batchId}/recipients/bulk`, `PATCH
        /payment-batch/{batchId}/recipients/{recipientId}`, `DELETE
        /payment-batch/{batchId}/recipients/{recipientId}` |
        `PAYMENT_METHOD.CREATE` |

        | `POST /payment-batch/{batchId}/withdraw` |
        `TRANSACTION.WITHDRAW.CREATE` |

        | `GET /payment-batch/{batchId}/sessions` | `TRANSACTION.HISTORY.READ` |

        | `POST /org/balance/topup`, `POST /webhooks/trigger` | None; any valid
        key |

````