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

# Mandates and recurring payments

This page covers charging a customer when they are not present: subscriptions, installments, or any payment you trigger without the customer typing in their card again.

There are two phases:

1. **Set up the mandate** — the customer is present, confirms a card payment, and consents to future off-session charges.
2. **Charge later** — the customer is absent. You charge the saved payment method directly.

## Prerequisites

You need:

* An **API key** from the Pesaswap dashboard.
* A **`profile_id`** from the dashboard.
* A **`customer_id`** for the customer you're charging.

## Phase 1: Set up the mandate

The customer is present for this call. They enter their card details and go through 3DS if the issuer requires it. Two things happen if the payment succeeds: the payment itself completes (or authorizes), and the payment method is saved for future off-session use.

Key fields:

* `payment_type: "new_mandate"` — tells the API this payment is establishing a mandate, not just a one-off charge.
* `setup_future_usage: "off_session"` — the payment method is saved for charges where the customer is not present.
* `authentication_type: "three_ds"` — forces 3DS on this payment. The mandate is created with the customer actually there to complete it.
* `mandate_data.customer_acceptance` — records that the customer agreed to future charges: `acceptance_type`, `accepted_at`, and the `online` object (`ip_address`, `user_agent`).
* `mandate_data.mandate_type.multi_use` — an `amount`/`currency` ceiling for the mandate. **This is not the amount charged now** — it's the maximum the mandate is allowed to authorize across all future charges, if your processor enforces one. The amount actually charged in this call is the top-level `amount`.

<Steps>
  <Step title="Create the mandate">
    <CodeGroup>
      ```bash curl theme={"system"}
      curl --request POST \
        --url https://api.sandbox.pesaswap.io/payments \
        --header "api-key: {{api_key}}" \
        --header "Content-Type: application/json" \
        --data '{
          "amount": 500,
          "currency": "KES",
          "confirm": true,
          "customer_id": "{{customer_id}}",
          "capture_method": "automatic",
          "setup_future_usage": "off_session",
          "authentication_type": "three_ds",
          "payment_method": "card",
          "payment_method_type": "debit",
          "payment_method_data": {
            "card": {
              "card_number": "{{card_number}}",
              "card_exp_month": "{{card_exp_month}}",
              "card_exp_year": "{{card_exp_year}}",
              "card_holder_name": "{{card_holder_name}}",
              "card_cvc": "{{card_cvc}}"
            }
          },
          "payment_type": "new_mandate",
          "return_url": "https://example.com/payments/complete",
          "mandate_data": {
            "customer_acceptance": {
              "acceptance_type": "online",
              "accepted_at": "2026-07-14T10:11:12Z",
              "online": {
                "ip_address": "{{ip_address}}",
                "user_agent": "{{user_agent}}"
              }
            },
            "mandate_type": {
              "multi_use": {
                "amount": 5000,
                "currency": "KES"
              }
            }
          }
        }'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch("https://api.sandbox.pesaswap.io/payments", {
        method: "POST",
        headers: {
          "api-key": "{{api_key}}",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          amount: 500,
          currency: "KES",
          confirm: true,
          customer_id: "{{customer_id}}",
          capture_method: "automatic",
          setup_future_usage: "off_session",
          authentication_type: "three_ds",
          payment_method: "card",
          payment_method_type: "debit",
          payment_method_data: {
            card: {
              card_number: "{{card_number}}",
              card_exp_month: "{{card_exp_month}}",
              card_exp_year: "{{card_exp_year}}",
              card_holder_name: "{{card_holder_name}}",
              card_cvc: "{{card_cvc}}",
            },
          },
          payment_type: "new_mandate",
          return_url: "https://example.com/payments/complete",
          mandate_data: {
            customer_acceptance: {
              acceptance_type: "online",
              accepted_at: "2026-07-14T10:11:12Z",
              online: {
                ip_address: "{{ip_address}}",
                user_agent: "{{user_agent}}",
              },
            },
            mandate_type: {
              multi_use: {
                amount: 5000,
                currency: "KES",
              },
            },
          },
        }),
      });

      const payment = await response.json();
      console.log(payment);
      ```

      ```python Python theme={"system"}
      import requests

      response = requests.post(
          "https://api.sandbox.pesaswap.io/payments",
          headers={
              "api-key": "{{api_key}}",
              "Content-Type": "application/json",
          },
          json={
              "amount": 500,
              "currency": "KES",
              "confirm": True,
              "customer_id": "{{customer_id}}",
              "capture_method": "automatic",
              "setup_future_usage": "off_session",
              "authentication_type": "three_ds",
              "payment_method": "card",
              "payment_method_type": "debit",
              "payment_method_data": {
                  "card": {
                      "card_number": "{{card_number}}",
                      "card_exp_month": "{{card_exp_month}}",
                      "card_exp_year": "{{card_exp_year}}",
                      "card_holder_name": "{{card_holder_name}}",
                      "card_cvc": "{{card_cvc}}",
                  },
              },
              "payment_type": "new_mandate",
              "return_url": "https://example.com/payments/complete",
              "mandate_data": {
                  "customer_acceptance": {
                      "acceptance_type": "online",
                      "accepted_at": "2026-07-14T10:11:12Z",
                      "online": {
                          "ip_address": "{{ip_address}}",
                          "user_agent": "{{user_agent}}",
                      },
                  },
                  "mandate_type": {
                      "multi_use": {
                          "amount": 5000,
                          "currency": "KES",
                      },
                  },
              },
          },
      )

      payment = response.json()
      print(payment)
      ```

      ```php PHP theme={"system"}
      <?php
      $curl = curl_init();

      curl_setopt_array($curl, [
          CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments",
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_CUSTOMREQUEST => "POST",
          CURLOPT_HTTPHEADER => [
              "api-key: {{api_key}}",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS => json_encode([
              "amount" => 500,
              "currency" => "KES",
              "confirm" => true,
              "customer_id" => "{{customer_id}}",
              "capture_method" => "automatic",
              "setup_future_usage" => "off_session",
              "authentication_type" => "three_ds",
              "payment_method" => "card",
              "payment_method_type" => "debit",
              "payment_method_data" => [
                  "card" => [
                      "card_number" => "{{card_number}}",
                      "card_exp_month" => "{{card_exp_month}}",
                      "card_exp_year" => "{{card_exp_year}}",
                      "card_holder_name" => "{{card_holder_name}}",
                      "card_cvc" => "{{card_cvc}}",
                  ],
              ],
              "payment_type" => "new_mandate",
              "return_url" => "https://example.com/payments/complete",
              "mandate_data" => [
                  "customer_acceptance" => [
                      "acceptance_type" => "online",
                      "accepted_at" => "2026-07-14T10:11:12Z",
                      "online" => [
                          "ip_address" => "{{ip_address}}",
                          "user_agent" => "{{user_agent}}",
                      ],
                  ],
                  "mandate_type" => [
                      "multi_use" => [
                          "amount" => 5000,
                          "currency" => "KES",
                      ],
                  ],
              ],
          ]),
      ]);

      $response = curl_exec($curl);
      curl_close($curl);

      echo $response;
      ```
    </CodeGroup>
  </Step>

  <Step title="Capture, if the mandate used manual capture">
    ### `capture_method`: automatic vs. manual

    This field decides whether a successful authorization also moves the money.

    * **`automatic`** (shown above) — funds are captured as soon as the payment succeeds. No further call needed.
    * **`manual`** — the payment authorizes the funds but does not take them. It lands in `requires_capture`, not `succeeded`. **An uncaptured payment is authorised, not settled** — the customer's funds are on hold, but nothing has moved to you. You must call `POST /payments/{payment_id}/capture` to actually capture the money.

    If you use `capture_method: "manual"` for the mandate setup, capture it with:

    <CodeGroup>
      ```bash curl theme={"system"}
      curl --request POST \
        --url https://api.sandbox.pesaswap.io/payments/{payment_id}/capture \
        --header "api-key: {{api_key}}" \
        --header "Content-Type: application/json" \
        --data '{}'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch(
        `https://api.sandbox.pesaswap.io/payments/${paymentId}/capture`,
        {
          method: "POST",
          headers: {
            "api-key": "{{api_key}}",
            "Content-Type": "application/json",
          },
          body: JSON.stringify({}),
        }
      );

      const payment = await response.json();
      console.log(payment);
      ```

      ```python Python theme={"system"}
      import requests

      response = requests.post(
          f"https://api.sandbox.pesaswap.io/payments/{payment_id}/capture",
          headers={
              "api-key": "{{api_key}}",
              "Content-Type": "application/json",
          },
          json={},
      )

      payment = response.json()
      print(payment)
      ```

      ```php PHP theme={"system"}
      <?php
      $curl = curl_init();

      curl_setopt_array($curl, [
          CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments/{$paymentId}/capture",
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_CUSTOMREQUEST => "POST",
          CURLOPT_HTTPHEADER => [
              "api-key: {{api_key}}",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS => json_encode([]),
      ]);

      $response = curl_exec($curl);
      curl_close($curl);

      echo $response;
      ```
    </CodeGroup>

    Pass `amount_to_capture` in the body to capture less than the full authorized amount. Omit it to capture the full amount.
  </Step>
</Steps>

## Phase 2: Charge later

The customer is not involved in this call. There's no card data, no redirect, no 3DS — you charge the payment method saved in Phase 1 directly.

Key fields:

* `payment_type: "recurring_mandate"` — tells the API this is a charge against a previously established mandate.
* `off_session: true` — confirms you're charging without the customer present.
* `recurring_details: { "type": "payment_method_id", "data": "{{payment_method_id}}" }` — points at the saved payment method.

`{{payment_method_id}}` is the identifier for the payment method that was saved during Phase 1, when that payment succeeded with `setup_future_usage: "off_session"`. Retrieve it from the Phase 1 payment's response, or from the customer's saved payment methods, and store it against the customer so you can reuse it here.

<CodeGroup>
  ```bash curl theme={"system"}
  curl --request POST \
    --url https://api.sandbox.pesaswap.io/payments \
    --header "api-key: {{api_key}}" \
    --header "Content-Type: application/json" \
    --data '{
      "amount": 500,
      "currency": "KES",
      "confirm": true,
      "customer_id": "{{customer_id}}",
      "off_session": true,
      "payment_type": "recurring_mandate",
      "profile_id": "{{profile_id}}",
      "description": "Monthly subscription charge",
      "recurring_details": {
        "type": "payment_method_id",
        "data": "{{payment_method_id}}"
      }
    }'
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch("https://api.sandbox.pesaswap.io/payments", {
    method: "POST",
    headers: {
      "api-key": "{{api_key}}",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount: 500,
      currency: "KES",
      confirm: true,
      customer_id: "{{customer_id}}",
      off_session: true,
      payment_type: "recurring_mandate",
      profile_id: "{{profile_id}}",
      description: "Monthly subscription charge",
      recurring_details: {
        type: "payment_method_id",
        data: "{{payment_method_id}}",
      },
    }),
  });

  const payment = await response.json();
  console.log(payment);
  ```

  ```python Python theme={"system"}
  import requests

  response = requests.post(
      "https://api.sandbox.pesaswap.io/payments",
      headers={
          "api-key": "{{api_key}}",
          "Content-Type": "application/json",
      },
      json={
          "amount": 500,
          "currency": "KES",
          "confirm": True,
          "customer_id": "{{customer_id}}",
          "off_session": True,
          "payment_type": "recurring_mandate",
          "profile_id": "{{profile_id}}",
          "description": "Monthly subscription charge",
          "recurring_details": {
              "type": "payment_method_id",
              "data": "{{payment_method_id}}",
          },
      },
  )

  payment = response.json()
  print(payment)
  ```

  ```php PHP theme={"system"}
  <?php
  $curl = curl_init();

  curl_setopt_array($curl, [
      CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments",
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_CUSTOMREQUEST => "POST",
      CURLOPT_HTTPHEADER => [
          "api-key: {{api_key}}",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "amount" => 500,
          "currency" => "KES",
          "confirm" => true,
          "customer_id" => "{{customer_id}}",
          "off_session" => true,
          "payment_type" => "recurring_mandate",
          "profile_id" => "{{profile_id}}",
          "description" => "Monthly subscription charge",
          "recurring_details" => [
              "type" => "payment_method_id",
              "data" => "{{payment_method_id}}",
          ],
      ]),
  ]);

  $response = curl_exec($curl);
  curl_close($curl);

  echo $response;
  ```
</CodeGroup>

## What to do next

Check the response `status`. If it's `succeeded`, the charge is done. If it's `requires_capture` because the mandate's `capture_method` was `manual`, call the capture endpoint above before you consider the money collected. If it's `processing`, poll `GET /payments/{payment_id}?force_sync=true` until it resolves.

## Status reference

| Status             | What it means                                                                                                         | What to do                 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `requires_capture` | The payment authorized under `capture_method: "manual"` and is waiting for your capture call. No money has moved yet. | Call the capture endpoint. |
| `processing`       | The connector is finalizing the result.                                                                               | Keep polling.              |
| `succeeded`        | The charge settled. The money has moved.                                                                              | Stop — settled.            |
| `failed`           | The charge was declined or the mandate/payment method could no longer be used.                                        | Stop — failed.             |

## Common errors

<AccordionGroup>
  <Accordion title="Charging off-session before the mandate exists">
    A Phase 2 call needs a `payment_method_id` from a Phase 1 payment that actually succeeded (or reached `requires_capture`). If Phase 1 never completed, there's nothing to charge.
  </Accordion>

  <Accordion title="Forgetting to capture a manual payment">
    A payment sitting in `requires_capture` looks fine in a dashboard glance but no funds have moved. If you use `capture_method: "manual"`, you must call `POST /payments/{payment_id}/capture` or the money never arrives.
  </Accordion>

  <Accordion title="Reusing a payment_method_id from a payment that never succeeded">
    If the Phase 1 payment failed or was cancelled, the payment method was never actually saved for off-session use, and Phase 2 charges against it will fail.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Refunds" icon="rotate-ccw" href="/essentials/payments-guide/refunds">
    Send money back to a customer.
  </Card>

  <Card title="Wallet payments" icon="wallet" href="/essentials/payments-guide/wallet-payments">
    M-Pesa, Airtel, and MTN MoMo.
  </Card>

  <Card title="Card payments and 3DS" icon="credit-card" href="/essentials/payments-guide/card-payments">
    The customer-present card flow this page builds on.
  </Card>

  <Card title="Quickstart" icon="rocket" href="/essentials/payments-guide/quickstart">
    Start here for your first payment.
  </Card>
</CardGroup>
