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

# Initiate B2C Payout

> Send money directly to a customer's M-Pesa mobile number.

Disburse funds to any M-Pesa number. Reversals are automatic on failure — no manual action needed.

<Warning>
  B2C payouts require **KYC approval** on your account. Requests before approval return `403 KYC_NOT_VERIFIED`. See [Going Live](/guides/going-live) for the KYC process.
</Warning>

## Key notes

* **Minimum amount:** KES 10.
* The B2C wallet must have sufficient balance — insufficient funds return `409 INSUFFICIENT_FUNDS`.
* The wallet balance is debited immediately when the request is accepted.
* On failure, the debited balance is **automatically reversed** to your B2C wallet.
* The service fee (from your service wallet) is deducted on initiation and **is not refunded** on failure.
* `transactionFee` in the response shows the service wallet amount reserved for this payout. It is `0` if no pricing rule is configured or the transaction fails.
* Payouts settle asynchronously. Expect 30 seconds to 5 minutes for completion.

## Receiving the result (`callbackUrl`)

Supply a `callbackUrl` in the request body. PalPluss will `POST` the transaction result to that URL when the payout reaches a terminal state (`SUCCESS` or `FAILED`).

```json theme={null}
{
  "amount": 500,
  "phone": "0712345678",
  "reference": "WD-2024-001",
  "callbackUrl": "https://yourserver.com/webhooks/b2c"
}
```

The callback payload sent to your URL:

```json theme={null}
{
  "event": "transaction.updated",
  "event_type": "transaction.success",
  "transaction": {
    "id": "fa98a577-95ea-4a8f-8467-1fbe74f5d6f4",
    "type": "B2C",
    "status": "SUCCESS",
    "amount": 500,
    "currency": "KES",
    "phone_number": "254712345678",
    "external_reference": "WD-2024-001",
    "mpesa_receipt": "RKL1XXXXX0",
    "transaction_fee": 5.00,
    "result_code": "0",
    "result_desc": "The service request is processed successfully.",
    "created_at": "2026-03-31T08:00:00.000Z",
    "updated_at": "2026-03-31T08:00:45.000Z"
  }
}
```

See [Webhooks](/guides/webhooks) for retry policy, idempotency handling, and full field reference.

## Wallet requirements

Before initiating a payout, verify:

* Your **B2C wallet** has enough balance for the payout amount
* Your **service wallet** has enough balance for the transaction fee

Both balances are visible in the console under **Finance → Wallets**.

## Failure and reversal

If M-Pesa declines or times out:

* PalPluss automatically credits your B2C wallet back
* Your webhook receives `status: "FAILED"` with `result_code` and `result_desc` from M-Pesa
* The reversed transaction shows `status: "REVERSED"`
* Retry safely — the original transaction is not retried automatically


## OpenAPI

````yaml POST /b2c/payouts
openapi: 3.1.0
info:
  title: PalPluss API
  version: '1.0'
  summary: >-
    Payment infrastructure for Kenya — STK Push, B2C payouts, and wallet
    management.
  description: >
    The PalPluss API gives developers programmatic access to M-Pesa STK Push
    collections,

    B2C payouts, service wallet top-ups, and payment channel management.


    **Base URL:** `https://api.palpluss.com/v1`


    All requests must be authenticated with an API key using HTTP Basic Auth.

    All responses are wrapped in a standard envelope. See [Errors](#tag/errors)
    for error codes.


    Amounts are in **KES (Kenyan Shillings)** unless stated otherwise. Phone
    numbers

    are normalized to the `254XXXXXXXXX` format internally.
  contact:
    name: PalPluss Developer Support
    email: developer@palpluss.com
    url: https://palpluss.com/developers
servers:
  - url: https://api.palpluss.com/v1
    description: Production
  - url: https://sandbox.palpluss.com/v1
    description: Sandbox
security:
  - BasicAuth: []
tags:
  - name: STK Push
    description: >
      Initiate M-Pesa STK Push payment prompts. The customer receives a PIN
      prompt on their

      phone and confirms payment. PalPluss delivers the result asynchronously to
      your webhook URL.
  - name: Transactions
    description: >-
      Query the status of any transaction by ID, or list all transactions with
      filters.
  - name: B2C Payouts
    description: >
      Send money directly to a customer's M-Pesa number. Requires KYC approval
      and sufficient

      B2C wallet balance. Payouts are processed asynchronously.
  - name: Service Wallet
    description: >
      The service wallet holds pre-funded tokens used to pay PalPluss
      transaction fees.

      Top up via STK Push. Balance is deducted with each API call.
  - name: Payment Channels
    description: >
      Payment channels represent the M-Pesa shortcodes (Paybill / Till)
      associated with your

      account. Channels are used to route STK Push requests to a specific
      shortcode.
paths:
  /b2c/payouts:
    post:
      tags:
        - B2C Payouts
      summary: Initiate B2C payout
      description: >
        Sends money directly to a customer's M-Pesa mobile number.


        **Requirements before calling this endpoint:**

        - Account KYC must be approved.

        - B2C wallet must have sufficient balance.

        - Minimum payout: KES 10.


        Payouts are processed **asynchronously**. The initial response confirms
        the

        payout was queued. The final result is delivered to your `callbackUrl`
        and the

        transaction status updates to `SUCCESS` or `FAILED`.


        On failure, the debited wallet balance is automatically reversed.
      operationId: initiateB2cPayout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/B2cPayoutRequest'
            examples:
              basic:
                summary: Basic payout
                value:
                  amount: 500
                  phone: '0712345678'
                  reference: WD-2024-001
                  description: Commission payout
                  callbackUrl: https://yourserver.com/webhooks/b2c
      responses:
        '200':
          description: Payout queued successfully. Transaction is in `PENDING` state.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/B2cPayoutResponse'
              examples:
                success:
                  value:
                    success: true
                    data:
                      transactionId: 2f9c1e77-0000-0000-0000-000000000001
                      tenantId: b6fbe75f-ce87-4d44-b318-6cfdb8b7de4d
                      channelId: null
                      type: B2C
                      status: PENDING
                      amount: 500
                      currency: KES
                      phone: '254712345678'
                      reference: WD-2024-001
                      description: Commission payout
                      resultDescription: B2C payout queued
                      createdAt: '2026-03-01T09:00:00.000Z'
                      updatedAt: '2026-03-01T09:00:00.000Z'
                    requestId: d2c3b4a5-0000-0000-0000-000000000000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '403':
          description: >
            Access denied. Possible reasons:

            - `ACCOUNT_INACTIVE` — your account is not active.

            - `KYC_NOT_VERIFIED` — KYC approval is required before making B2C
            payouts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                kyc_required:
                  value:
                    success: false
                    error:
                      message: KYC verification is required to initiate payouts.
                      code: KYC_NOT_VERIFIED
                      details: {}
                    requestId: d2c3b4a5-0000-0000-0000-000000000000
        '409':
          description: Conflict — wallet inactive or insufficient funds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    B2cPayoutRequest:
      type: object
      required:
        - amount
        - phone
        - reference
      properties:
        amount:
          type: number
          minimum: 10
          description: Payout amount in KES. Minimum KES 10.
          example: 500
        currency:
          type: string
          default: KES
          description: Currency code. Currently only `KES` is supported.
        phone:
          type: string
          description: Recipient M-Pesa phone number.
          example: '0712345678'
        reference:
          type: string
          description: Your reference for this payout (e.g. withdrawal ID).
          example: WD-2024-001
        description:
          type: string
          description: Short description of the payout purpose.
          example: Commission payout
        channelId:
          type: string
          format: uuid
          nullable: true
          description: Optional channel to use for this payout.
        callbackUrl:
          type: string
          format: uri
          description: >
            HTTPS URL to receive the payout result. PalPluss POSTs the terminal

            transaction payload here when the status reaches `SUCCESS` or
            `FAILED`.
          example: https://yourserver.com/webhooks/b2c
    SuccessEnvelope:
      type: object
      required:
        - success
        - data
        - requestId
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          description: Response payload. Shape varies by endpoint.
        requestId:
          type: string
          format: uuid
          description: Unique identifier for this API request. Include in support tickets.
          example: c1b2a3d4-e5f6-7890-abcd-ef1234567890
    B2cPayoutResponse:
      type: object
      properties:
        transactionId:
          type: string
          format: uuid
        tenantId:
          type: string
          format: uuid
        channelId:
          type: string
          format: uuid
          nullable: true
        type:
          type: string
          example: B2C
        status:
          type: string
          example: PENDING
        amount:
          type: number
          example: 500
        currency:
          type: string
          example: KES
        phone:
          type: string
          example: '254712345678'
        reference:
          type: string
          example: WD-2024-001
        description:
          type: string
          nullable: true
        resultDescription:
          type: string
          example: B2C payout queued
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ErrorEnvelope:
      type: object
      required:
        - success
        - error
        - requestId
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required:
            - message
            - code
          properties:
            message:
              type: string
              description: Human-readable error description.
              example: Invalid API key
            code:
              type: string
              description: Machine-readable error code.
              example: INVALID_API_KEY
            details:
              type: object
              description: Optional structured details about the error.
        requestId:
          type: string
          format: uuid
          example: c1b2a3d4-e5f6-7890-abcd-ef1234567890
  responses:
    BadRequest:
      description: Validation error — check the request body or parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            invalid_phone:
              value:
                success: false
                error:
                  message: Invalid phone number format.
                  code: INVALID_PHONE
                  details: {}
                requestId: a1b2c3d4-0000-0000-0000-000000000000
    Unauthorized:
      description: Missing, invalid, or revoked API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            invalid_key:
              value:
                success: false
                error:
                  message: Invalid or revoked API key.
                  code: INVALID_API_KEY
                  details: {}
                requestId: a1b2c3d4-0000-0000-0000-000000000000
    InsufficientBalance:
      description: Service wallet balance is too low to cover the transaction fee.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            low_balance:
              value:
                success: false
                error:
                  message: >-
                    Insufficient service wallet balance to cover the transaction
                    fee.
                  code: INSUFFICIENT_SERVICE_BALANCE
                  details: {}
                requestId: a1b2c3d4-0000-0000-0000-000000000000
    RateLimited:
      description: Rate limit exceeded — 60 requests per minute per API key.
      headers:
        x-ratelimit-limit:
          schema:
            type: integer
          description: Maximum requests per minute.
        x-ratelimit-remaining:
          schema:
            type: integer
          description: Remaining requests in the current window.
        x-ratelimit-reset:
          schema:
            type: integer
          description: Unix timestamp when the window resets.
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            rate_limited:
              value:
                success: false
                error:
                  message: Rate limit exceeded. Try again in 12 seconds.
                  code: RATE_LIMIT_EXCEEDED
                  details:
                    limit: 60
                    remaining: 0
                    resetInSeconds: 12
                requestId: a1b2c3d4-0000-0000-0000-000000000000
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
      description: >
        Paste your PalPluss API key in the **username** field and leave the

        **password** field empty — it is ignored.


        Get your key from **console → Settings → API Keys** (starts with
        `pk_live_` or `pk_test_`).


        In your own code, send the key directly:

        `Authorization: Basic YOUR_API_KEY`

````