> ## 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 STK Push

> Send an M-Pesa payment prompt to a customer's phone.

Trigger a payment. The customer receives a PIN prompt on their phone within seconds.

<Note>
  A transaction fee is deducted from your [service wallet](/api-reference/wallets/balance) when the request is accepted. Verify your balance before calling this endpoint.
</Note>

## Key notes

* Phone numbers are accepted in `07XXXXXXXX`, `01XXXXXXXX`, `+254XXXXXXXXX`, or `254XXXXXXXXX` format and normalised internally.
* `accountReference` must be 12 characters or fewer — it appears on the customer's M-Pesa statement.
* `transactionDesc` must be 13 characters or fewer — it appears on the customer's PIN prompt.
* Save the returned `transactionId` — use it to poll status or match incoming webhooks.
* `PENDING` means the STK prompt was sent. The final result arrives via your `callbackUrl`.
* `transactionFee` in the response shows the service wallet amount deducted for this request. It is `0` when no pricing rule is configured.
* **A payment channel is required.** If you omit `channelId`, your account's default channel is used. If your account has no channels configured, the request returns `400 NO_PAYMENT_CHANNELS`. If channels exist but none is set as default, the request returns `400 NO_DEFAULT_CHANNEL`.

## Channel requirement

Every STK Push must be routed through a payment channel on your account. The channel determines which M-Pesa shortcode (Paybill or Till) the customer pays to.

| Scenario                                            | Behaviour                                         |
| --------------------------------------------------- | ------------------------------------------------- |
| `channelId` provided and found                      | Payment routed to that channel's shortcode        |
| `channelId` omitted, default channel exists         | Payment routed to the default channel's shortcode |
| `channelId` omitted, no default channel             | `400 NO_DEFAULT_CHANNEL`                          |
| `channelId` omitted, account has no channels at all | `400 NO_PAYMENT_CHANNELS`                         |
| `channelId` provided but not found on account       | `400 CHANNEL_NOT_FOUND`                           |

<Tip>
  Create and manage your payment channels from the **console → Payment Channels**. Set one as default so requests without an explicit `channelId` always route correctly.
</Tip>

## Error codes

| Code                           | HTTP | What it means                                   | What to do                                                            |
| ------------------------------ | ---- | ----------------------------------------------- | --------------------------------------------------------------------- |
| `NO_PAYMENT_CHANNELS`          | 400  | Account has no payment channels                 | Create a payment channel in the console first.                        |
| `NO_DEFAULT_CHANNEL`           | 400  | Channels exist but none is set as default       | Set a default channel in the console, or pass `channelId` explicitly. |
| `CHANNEL_NOT_FOUND`            | 400  | Provided `channelId` not found on account       | Verify the channel ID in the console.                                 |
| `INVALID_PHONE`                | 400  | Phone number format not recognised              | Use `07XXXXXXXX`, `01XXXXXXXX`, or `254XXXXXXXXX`.                    |
| `INSUFFICIENT_SERVICE_BALANCE` | 402  | Service wallet too low to cover the fee         | Top up the service wallet and retry.                                  |
| `STK_TEMP_BANNED`              | 429  | STK temporarily blocked due to low success rate | Wait `details.retryAfterSeconds` before retrying.                     |

***

## Using a credential profile (`credential_id`)

The optional `credential_id` field lets you route the STK Push through your own M-Pesa Daraja credentials (BYOC — Bring Your Own Credentials) instead of the platform default.

When `credential_id` is provided:

* Daraja OAuth and the STK Push initiation use that profile's `consumerKey`, `consumerSecret`, `passkey`, and `shortcode`.
* **With a channel (`channelId`)** — the channel type determines the transaction type (`PAYBILL` → `CustomerPayBillOnline`, `TILL` → `CustomerBuyGoodsOnline`). The channel's shortcode is used as `PartyB`.
* **Without a channel** — the credential profile's `transactionType` (set when creating the profile) determines the transaction type. The profile's shortcode is used as `PartyB`. Falls back to the platform `DARAJA_TRANSACTION_TYPE` setting if the profile has no `transactionType` set.

<Note>
  Credential profiles are managed from the merchant dashboard under **Settings → Credential Profiles**. Create a `MERCHANT_BYOC` profile and copy its UUID to use here.
</Note>

## Response fields

| Field                | Type           | Description                                                         |
| -------------------- | -------------- | ------------------------------------------------------------------- |
| `transactionId`      | string         | UUID of the created transaction                                     |
| `type`               | string         | Always `"STK"`                                                      |
| `status`             | string         | Always `"PENDING"` on initiation                                    |
| `amount`             | number         | Payment amount in KES                                               |
| `currency`           | string         | Always `"KES"`                                                      |
| `phone`              | string         | Normalised phone number (`254XXXXXXXXX`)                            |
| `accountReference`   | string         | Reference shown on customer's M-Pesa statement                      |
| `transactionDesc`    | string         | Description shown on customer's PIN prompt                          |
| `transactionFee`     | number         | Service wallet fee charged for this request. `0` if no fee applies. |
| `providerRequestId`  | string \| null | Safaricom MerchantRequestID                                         |
| `providerCheckoutId` | string \| null | Safaricom CheckoutRequestID                                         |
| `resultCode`         | string \| null | Provider result code (`"0"` = accepted)                             |
| `resultDescription`  | string \| null | Provider result message                                             |
| `createdAt`          | string         | ISO 8601 timestamp                                                  |
| `updatedAt`          | string         | ISO 8601 timestamp                                                  |

## STK abuse protection

If your API key initiates too many STK Pushes with a low success rate in a short window, further requests return `429 STK_TEMP_BANNED`. The ban lasts 1 hour. Check `details.retryAfterSeconds` for the exact wait time.


## OpenAPI

````yaml POST /payments/stk
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:
  /payments/stk:
    post:
      tags:
        - STK Push
      summary: Initiate STK Push
      description: >
        Sends an M-Pesa STK Push prompt to the customer's phone. The customer
        enters

        their M-Pesa PIN to authorise payment.


        Payment results are delivered **asynchronously** to the `callbackUrl`
        you supply.

        Poll `GET /transactions/{id}` if you need to check status
        programmatically.


        **Fees:** A transaction fee is deducted from your service wallet balance
        at the

        time the request is accepted. If your balance is insufficient the
        request returns `402`.
      operationId: initiateStk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StkInitiateRequest'
            examples:
              basic:
                summary: Basic STK Push
                value:
                  amount: 1000
                  phone: '0712345678'
                  accountReference: INV-2024-001
                  transactionDesc: 'Payment for invoice #2024-001'
                  channelId: your-payment-channel-id
                  callbackUrl: https://yourserver.com/webhooks/mpesa
              with_channel:
                summary: With explicit channel
                value:
                  amount: 500
                  phone: '0712345678'
                  accountReference: ORD-0042
                  transactionDesc: Order payment
                  channelId: 8b3f1a2c-0000-0000-0000-000000000000
                  callbackUrl: https://yourserver.com/webhooks/mpesa
              with_credential:
                summary: With BYOC credential profile
                value:
                  amount: 500
                  phone: '0712345678'
                  accountReference: ORD-0043
                  transactionDesc: Order payment
                  credential_id: a1b2c3d4-0000-0000-0000-000000000001
                  callbackUrl: https://yourserver.com/webhooks/mpesa
      responses:
        '200':
          description: STK Push accepted. Transaction is in `PENDING` state.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/StkInitiateResponse'
              examples:
                success:
                  value:
                    success: true
                    data:
                      transactionId: fa98a577-95ea-4a8f-8467-1fbe74f5d6f4
                      tenantId: b6fbe75f-ce87-4d44-b318-6cfdb8b7de4d
                      channelId: null
                      type: STK
                      status: PENDING
                      amount: 1000
                      currency: KES
                      phone: '254712345678'
                      accountReference: INV-2024-001
                      transactionDesc: 'Payment for invoice #2024-001'
                      providerRequestId: 29115-34620561-1
                      providerCheckoutId: ws_CO_191220191020363925
                      transactionFee: 2.5
                      resultCode: '0'
                      resultDescription: Success. Request accepted for processing
                      createdAt: '2026-03-01T08:00:00.000Z'
                      updatedAt: '2026-03-01T08:00:00.000Z'
                    requestId: c1b2a3d4-0000-0000-0000-000000000000
        '400':
          description: Bad request — validation error or channel not configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                no_payment_channels:
                  summary: Account has no payment channels
                  value:
                    success: false
                    error:
                      message: >-
                        Your account has no payment channels configured. Please
                        create a payment channel in your dashboard before
                        initiating payments.
                      code: NO_PAYMENT_CHANNELS
                      details: {}
                    requestId: c1b2a3d4-0000-0000-0000-000000000000
                no_default_channel:
                  summary: Channels exist but none is set as default
                  value:
                    success: false
                    error:
                      message: >-
                        No default payment channel is set on your account. Set
                        one as default in your dashboard, or pass a channelId in
                        your request.
                      code: NO_DEFAULT_CHANNEL
                      details: {}
                    requestId: c1b2a3d4-0000-0000-0000-000000000000
                channel_not_found:
                  summary: Provided channelId not found
                  value:
                    success: false
                    error:
                      message: channelId not found for tenant
                      code: CHANNEL_NOT_FOUND
                      details:
                        channelId: 8b3f1a2c-0000-0000-0000-000000000000
                    requestId: c1b2a3d4-0000-0000-0000-000000000000
                invalid_phone:
                  summary: Invalid phone number
                  value:
                    success: false
                    error:
                      message: Invalid phone
                      code: INVALID_PHONE
                      details:
                        phone: '123'
                    requestId: c1b2a3d4-0000-0000-0000-000000000000
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    StkInitiateRequest:
      type: object
      required:
        - amount
        - phone
        - accountReference
        - transactionDesc
        - callbackUrl
      properties:
        amount:
          type: number
          format: decimal
          minimum: 1
          description: Amount to charge in KES.
          example: 1000
        phone:
          type: string
          description: >
            Customer phone number. Accepted formats: `0712345678`,
            `254712345678`,

            `+254712345678`. Normalised to `254XXXXXXXXX` internally.
          example: '0712345678'
        accountReference:
          type: string
          maxLength: 12
          description: Your reference — invoice number, order ID, etc.
          example: INV-2024-001
        transactionDesc:
          type: string
          maxLength: 13
          description: Short description shown on the customer's PIN prompt.
          example: Payment
        callbackUrl:
          type: string
          format: uri
          description: |
            HTTPS URL where PalPluss will POST the transaction result after the
            customer confirms or cancels payment.
          example: https://yourserver.com/webhooks/mpesa
        channelId:
          type: string
          format: uuid
          nullable: true
          description: >
            Optional. Route the STK Push through a specific payment channel
            (shortcode).

            If omitted, the default channel is used.
        credential_id:
          type: string
          format: uuid
          nullable: true
          description: >
            Optional. Use a specific MERCHANT_BYOC credential profile for this
            request.

            When provided, Daraja OAuth and STK initiation use that profile's
            credentials

            (`consumerKey`, `consumerSecret`, `passkey`, `shortcode`).


            **Transaction type (`commandId`) resolution when `credential_id` is
            set:**

            - Channel present → channel type drives it (`PAYBILL` =
            CustomerPayBillOnline, `TILL` = CustomerBuyGoodsOnline)

            - No channel → profile's `transactionType` is used when set; falls
            back to platform default


            **`PartyB` resolution when no channel is configured:**

            the credential profile's shortcode is used instead of the platform
            default shortcode.
    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
    StkInitiateResponse:
      type: object
      properties:
        transactionId:
          type: string
          format: uuid
        tenantId:
          type: string
          format: uuid
        channelId:
          type: string
          format: uuid
          nullable: true
        type:
          type: string
          example: STK
        status:
          type: string
          example: PENDING
        amount:
          type: number
          example: 1000
        currency:
          type: string
          example: KES
        phone:
          type: string
          example: '254712345678'
        accountReference:
          type: string
          example: INV-2024-001
        transactionDesc:
          type: string
          example: Payment
        providerRequestId:
          type: string
          nullable: true
        providerCheckoutId:
          type: string
          nullable: true
        transactionFee:
          type: number
          description: >
            Service wallet fee charged for this request in KES.

            `0` when no pricing rule is configured or the transaction did not
            succeed.
          example: 2.5
        resultCode:
          type: string
          nullable: true
        resultDescription:
          type: string
          nullable: true
        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:
    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
    NotFound:
      description: The requested resource does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            not_found:
              value:
                success: false
                error:
                  message: Transaction not found.
                  code: TRANSACTION_NOT_FOUND
                  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`

````