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

# Schedule recurring payments to beneficiaries and wallets

> Set up recurring PeerPesa payouts in three steps: basic info, schedule, and review. Pay beneficiaries or wallets daily, weekly, or monthly from your balance.

Recurring payments let you pay the same beneficiary or wallet on a fixed schedule from your PeerPesa balance, for example a monthly KES salary to an M-Pesa number or a weekly USDC transfer to a Stellar address. Setup follows three steps: **Basic Info** (who, what, and how much), **Schedule** (when and how often), and **Review** (check upcoming runs and fees, then activate).

<Steps>
  <Step title="Basic Info">
    Create a draft with `POST /recurring-payments`: name, payment type, source coin and network, amount, and recipient.
  </Step>

  <Step title="Schedule">
    Set frequency, start date, run time, and when it ends with `PUT /recurring-payments/{id}/schedule`.
  </Step>

  <Step title="Review">
    Call `GET /recurring-payments/{id}/review` to see the next run dates, estimated fees, and balance check. Then activate with `POST /recurring-payments/{id}/activate`.
  </Step>
</Steps>

<Note>
  Rates are not locked for recurring payments. Each run creates a fresh [quote](/api-reference/quotes) at execution time and confirms it automatically, so the fee and rate shown in Review are estimates.
</Note>

## Statuses

| Status | Meaning |
| - | - |
| `draft` | Basic info saved; schedule or activation still pending. |
| `active` | Runs execute on schedule. |
| `paused` | No runs execute until restarted. Set manually or after 3 failed runs in a row. |
| `completed` | The last scheduled run finished. |
| `deleted` | Removed. No further runs execute. Past transactions are kept. |

## Step 1: Basic Info

**POST** `/recurring-payments`

Creates a recurring payment in `draft` status.

<ParamField body="name" type="string" required>
  Label shown in your dashboard, for example `"Monthly rent"`.
</ParamField>

<ParamField body="type" type="string" required>
  `withdraw` (pay fiat to a beneficiary, see [Withdraw](/api-reference/withdraw)) or `send` (send crypto to a wallet or PeerPesa user, see [Send](/api-reference/send)).
</ParamField>

<ParamField body="source" type="object" required>
  Balance to pay from.

  <Expandable title="properties">
    <ParamField body="symbol" type="string" required>Coin symbol, for example `USDC`.</ParamField>
    <ParamField body="network" type="string" required>Network code, for example `STELLAR`. Must be `active` in [GET /currencies](/api-reference/coins).</ParamField>
  </Expandable>
</ParamField>

<ParamField body="amount" type="string" required>
  Amount per run as a decimal string.
</ParamField>

<ParamField body="amount_currency" type="string" required>
  Currency of `amount`: the source `symbol`, or the beneficiary's fiat code for `withdraw` (for example `KES` to pay a fixed fiat amount each run).
</ParamField>

<ParamField body="recipient" type="object" required>
  Who gets paid.

  <Expandable title="properties">
    <ParamField body="beneficiary_id" type="string">Required for `withdraw`. A saved [beneficiary](/api-reference/beneficiaries).</ParamField>
    <ParamField body="destination" type="object">Required for `send`. Same shape as the `destination` object on [Send](/api-reference/send).</ParamField>
  </Expandable>
</ParamField>

<ParamField body="description" type="string">
  Note attached to every payment, shown to the recipient where the method supports it.
</ParamField>

<ParamField body="reference" type="string">
  Your own identifier. Each run's transaction gets `reference` plus a run number suffix, for example `rent-2026-003`.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.peerpesa.co/v1/recurring-payments \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Monthly rent",
      "type": "withdraw",
      "source": { "symbol": "USDC", "network": "STELLAR" },
      "amount": "25000.00",
      "amount_currency": "KES",
      "recipient": { "beneficiary_id": "ben_3m7q2c" },
      "description": "Rent for Apt 4B",
      "reference": "rent-2026"
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://api.peerpesa.co/v1/recurring-payments", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "Monthly rent",
      type: "withdraw",
      source: { symbol: "USDC", network: "STELLAR" },
      amount: "25000.00",
      amount_currency: "KES",
      recipient: { beneficiary_id: "ben_3m7q2c" },
      description: "Rent for Apt 4B",
      reference: "rent-2026",
    }),
  });
  const recurring = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "name": "Monthly rent",
  "type": "withdraw",
  "status": "draft",
  "source": { "symbol": "USDC", "network": "STELLAR" },
  "amount": "25000.00",
  "amount_currency": "KES",
  "recipient": { "beneficiary_id": "ben_3m7q2c" },
  "description": "Rent for Apt 4B",
  "reference": "rent-2026",
  "schedule": null,
  "created_at": "2026-10-15T08:00:00Z"
}
```

## Step 2: Schedule

**PUT** `/recurring-payments/{id}/schedule`

Sets when and how often the payment runs. You can call it again while the payment is `draft` or `paused`.

<ParamField path="id" type="string" required>
  Recurring payment ID, for example `rp_5t8w2k`.
</ParamField>

<ParamField body="frequency" type="string" required>
  `daily`, `weekly`, `biweekly`, or `monthly`.
</ParamField>

<ParamField body="day_of_week" type="string">
  Required for `weekly` and `biweekly`: `monday` to `sunday`.
</ParamField>

<ParamField body="day_of_month" type="integer">
  Required for `monthly`: `1` to `31`. In shorter months, runs on the last day.
</ParamField>

<ParamField body="start_date" type="string" required>
  First possible run date (`YYYY-MM-DD`). Must be today or later.
</ParamField>

<ParamField body="time" type="string" default="09:00">
  Run time in 24-hour `HH:MM` format.
</ParamField>

<ParamField body="timezone" type="string" default="Africa/Nairobi">
  IANA timezone for `start_date` and `time`.
</ParamField>

<ParamField body="end" type="object" required>
  When the schedule stops. Set `type` to `never`, `on_date` (with `date`), or `after_occurrences` (with `occurrences`).
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PUT https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/schedule \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "frequency": "monthly",
      "day_of_month": 1,
      "start_date": "2026-11-01",
      "time": "09:00",
      "timezone": "Africa/Nairobi",
      "end": { "type": "after_occurrences", "occurrences": 12 }
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/schedule",
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        frequency: "monthly",
        day_of_month: 1,
        start_date: "2026-11-01",
        time: "09:00",
        timezone: "Africa/Nairobi",
        end: { type: "after_occurrences", occurrences: 12 },
      }),
    }
  );
  const recurring = await res.json();
  ```
</CodeGroup>

Returns the recurring payment with its `schedule` filled in. Status stays `draft`.

## Step 3: Review and activate

### GET /recurring-payments/\{id}/review

Returns a summary to show the user before activation: recipient, upcoming run dates, estimated fees at current rates, and whether the balance covers the next run.

```bash cURL theme={null}
curl https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/review \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "name": "Monthly rent",
  "type": "withdraw",
  "recipient": {
    "beneficiary_id": "ben_3m7q2c",
    "name": "Jane Wanjiru",
    "type": "phone_number",
    "account": "+2547****5678"
  },
  "per_run": {
    "amount": "25000.00",
    "amount_currency": "KES",
    "estimated_rate": "129.45",
    "estimated_source_amount": "193.13",
    "estimated_fee": {
      "total": "1.50",
      "currency": "USDC",
      "processing_fee": "1.00",
      "network_fee": "0.50"
    },
    "estimated_total_debit": "194.63",
    "source_currency": "USDC"
  },
  "schedule_summary": "Monthly on day 1 at 09:00 (Africa/Nairobi), 12 payments",
  "next_runs": [
    "2026-11-01T06:00:00Z",
    "2026-12-01T06:00:00Z",
    "2027-01-01T06:00:00Z"
  ],
  "total_runs": 12,
  "balance_check": {
    "available": "1250.40",
    "covers_next_run": true
  },
  "warnings": []
}
```

<ResponseField name="per_run" type="object">
  Estimated cost of one run at current rates, including the fee breakdown and the total debited from your balance.
</ResponseField>

<ResponseField name="next_runs" type="string[]">
  The next three run times in UTC.
</ResponseField>

<ResponseField name="balance_check" type="object">
  Your available balance on the source coin and network, and whether it covers the next run.
</ResponseField>

<ResponseField name="warnings" type="string[]">
  Issues to show the user, for example `insufficient_balance_for_next_run` or `amount_exceeds_monthly_limit`.
</ResponseField>

### POST /recurring-payments/\{id}/activate

Activates a reviewed draft. Status becomes `active` and `next_run_at` is set.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/activate \
    -H "Authorization: Bearer $ACCESS_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/activate",
    { method: "POST", headers: { Authorization: `Bearer ${accessToken}` } }
  );
  const recurring = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "status": "active",
  "next_run_at": "2026-11-01T06:00:00Z",
  "runs_completed": 0,
  "total_runs": 12,
  "activated_at": "2026-10-15T08:04:00Z"
}
```

## How runs execute

At each run time, PeerPesa creates a quote, confirms it, and debits your balance. The result is a normal [order](/api-reference/orders) with `recurring_payment` set to the schedule ID.

* If the balance is too low, the run fails with `insufficient_balance` and is not retried.
* After 3 failed runs in a row, the recurring payment is set to `paused`.
* You receive `recurring_payment.run_succeeded` or `recurring_payment.run_failed` [webhooks](/webhooks) for every run, and `recurring_payment.run_upcoming` 24 hours before.

## Pause a recurring payment

**POST** `/recurring-payments/{id}/pause`

Stops future runs of an `active` recurring payment without deleting it. A run already in progress finishes normally.

<ParamField path="id" type="string" required>
  Recurring payment ID, for example `rp_5t8w2k`.
</ParamField>

<ParamField body="reason" type="string">
  Optional note stored with the pause, for example `"Tenant away in December"`.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/pause \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "reason": "Tenant away in December" }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/pause",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ reason: "Tenant away in December" }),
    }
  );
  const recurring = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "status": "paused",
  "paused_at": "2026-11-20T10:12:00Z",
  "pause_reason": "Tenant away in December",
  "next_run_at": null,
  "runs_completed": 1,
  "total_runs": 12
}
```

## Restart a recurring payment

**POST** `/recurring-payments/{id}/restart`

Restarts a `paused` recurring payment. Runs missed while paused are skipped, not paid in arrears, and the next run follows the existing schedule. Skipped runs do not count toward `after_occurrences`.

<ParamField path="id" type="string" required>
  Recurring payment ID.
</ParamField>

<ParamField body="start_date" type="string">
  Optional new first run date (`YYYY-MM-DD`). Defaults to the next date on the existing schedule.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/restart \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "start_date": "2027-01-01" }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/restart",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ start_date: "2027-01-01" }),
    }
  );
  const recurring = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "status": "active",
  "restarted_at": "2026-12-28T08:30:00Z",
  "next_run_at": "2027-01-01T06:00:00Z",
  "runs_completed": 1,
  "total_runs": 12
}
```

<Tip>
  To change the amount or schedule before restarting, call `PATCH /recurring-payments/{id}` or `PUT /recurring-payments/{id}/schedule` while the payment is `paused`.
</Tip>

## Delete a recurring payment

**DELETE** `/recurring-payments/{id}`

Deletes a recurring payment. No further runs execute and it can no longer be restarted. Transactions from past runs are kept and stay available through the transactions endpoint below and [GET /orders](/api-reference/orders).

<ParamField path="id" type="string" required>
  Recurring payment ID.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k \
    -H "Authorization: Bearer $ACCESS_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k",
    { method: "DELETE", headers: { Authorization: `Bearer ${accessToken}` } }
  );
  const result = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rp_5t8w2k",
  "status": "deleted",
  "deleted_at": "2026-12-28T09:00:00Z",
  "runs_completed": 1
}
```

<Warning>
  Deleting cannot be undone. Pause instead if you may need the schedule again.
</Warning>

## List transactions for a schedule

**GET** `/recurring-payments/{id}/transactions`

Returns every transaction created by one recurring payment, newest first. Failed runs appear too, with `status: "failed"` and a `failure_code`.

<ParamField path="id" type="string" required>
  Recurring payment ID.
</ParamField>

<ParamField query="status" type="string">
  Filter by transaction status: `pending`, `processing`, `completed`, `failed`, or `refunded`.
</ParamField>

<ParamField query="search" type="string">
  Matches transaction ID, `reference`, and `tx_hash`. Minimum 2 characters.
</ParamField>

<ParamField query="start_date" type="string">
  Only include runs on or after this date (`YYYY-MM-DD` or ISO 8601).
</ParamField>

<ParamField query="end_date" type="string">
  Only include runs on or before this date (`YYYY-MM-DD` or ISO 8601).
</ParamField>

<ParamField query="sort_order" default="desc" type="string">
  `desc` for newest run first, `asc` for oldest first.
</ParamField>

<ParamField query="page" default="1" type="integer">
  Page number to return, starting at `1`. See [Pagination](/pagination).
</ParamField>

<ParamField query="size" default="20" type="integer">
  Results per page, up to 100. `limit` is accepted as an alias.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/transactions?page=1&size=10" \
    -H "Authorization: Bearer $ACCESS_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.peerpesa.co/v1/recurring-payments/rp_5t8w2k/transactions?page=1&size=10",
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );
  const { data, pagination, summary } = await res.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "recurring_payment_id": "rp_5t8w2k",
  "data": [
    {
      "id": "txn_r7k2m1",
      "type": "withdraw",
      "status": "failed",
      "run_number": 2,
      "scheduled_for": "2026-12-01T06:00:00Z",
      "symbol": "USDC",
      "network": "STELLAR",
      "amount": "0.00",
      "fee": null,
      "failure_code": "insufficient_balance",
      "reference": "rent-2026-002",
      "created_at": "2026-12-01T06:00:02Z"
    },
    {
      "id": "txn_q4c9x8",
      "type": "withdraw",
      "status": "completed",
      "run_number": 1,
      "scheduled_for": "2026-11-01T06:00:00Z",
      "symbol": "USDC",
      "network": "STELLAR",
      "amount": "193.13",
      "fiat_currency": "KES",
      "destination_amount": "25000.00",
      "rate": "129.45",
      "fee": {
        "total": "1.50",
        "currency": "USDC",
        "processing_fee": "1.00",
        "network_fee": "0.50"
      },
      "failure_code": null,
      "reference": "rent-2026-001",
      "created_at": "2026-11-01T06:00:03Z"
    }
  ],
  "summary": {
    "runs_completed": 1,
    "runs_failed": 1,
    "total_paid": "25000.00",
    "total_paid_currency": "KES",
    "total_fees": "1.50",
    "total_fees_currency": "USDC"
  },
  "pagination": {
    "page": 1,
    "size": 10,
    "total": 2,
    "total_pages": 1
  }
}
```

<ResponseField name="data[].run_number" type="integer">
  Position of this run in the schedule, starting at 1.
</ResponseField>

<ResponseField name="data[].scheduled_for" type="string">
  When the run was scheduled to execute (UTC).
</ResponseField>

<ResponseField name="data[].failure_code" type="string | null">
  Error code for failed runs, for example `insufficient_balance`. `null` otherwise.
</ResponseField>

<ResponseField name="summary" type="object">
  Totals across all runs of this schedule: completed and failed run counts, total paid, and total fees.
</ResponseField>

<ResponseField name="pagination" type="object">
  Page details: `page`, `size`, `total`, and `total_pages`. The `summary` totals always cover all runs, not just the current page.
</ResponseField>

Each item is a transaction like those in the [order object](/api-reference/order-details). You can also list the orders a schedule created with `GET /orders?recurring_payment=rp_5t8w2k`.

## Other endpoints

| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/recurring-payments` | List recurring payments. Filter by `status`, `type`, `search`, `start_date`, and `end_date`; paginate with `page` and `size`. |
| `GET` | `/recurring-payments/{id}` | Retrieve one recurring payment. |
| `PATCH` | `/recurring-payments/{id}` | Update basic info (`name`, `amount`, `description`). Changes apply from the next run. |

## Errors

| Code | Status | Cause |
| - | - | - |
| `invalid_schedule` | 422 | Missing `day_of_week` or `day_of_month`, or `start_date` in the past. |
| `schedule_required` | 422 | Activation attempted before a schedule was set. |
| `invalid_status_transition` | 409 | For example, pausing a payment that is not `active`, or restarting a deleted one. |
| `recurring_payment_not_found` | 404 | No recurring payment with this ID exists for your account. |
| `beneficiary_mismatch` | 422 | Beneficiary currency does not match `amount_currency`. |
| `unsupported_network` | 422 | The source network is not `active`. |
| `insufficient_balance` | 422 | A run could not be funded. |

See [Error Codes](/errors) for the full list.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.