Payment Plans API
Overview
The Payment Plans API lets you programmatically create, manage, and control recurring billing schedules. A payment plan generates and processes payment intents automatically on your chosen frequency.
For background on how plans work, see Understanding Plans.
All payment plan operations require a secret key (sk_xxx) with payment_plans:read or payment_plans:write scope.
Create a payment plan
curl -X POST https://api.elasticpay.co/api/v1/payment_plans \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "customer_id": "cus_0abc123def456ghi789jkl012mno", "name": "Monthly gym membership", "frequency_type": "monthly", "recurring_amount_cents": 4900, "currency": "AUD", "start_date": "2026-05-01", "until_further_notice": true }'const res = await fetch("https://api.elasticpay.co/api/v1/payment_plans", { method: "POST", headers: { "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ customer_id: "cus_0abc123def456ghi789jkl012mno", name: "Monthly gym membership", frequency_type: "monthly", recurring_amount_cents: 4900, currency: "AUD", start_date: "2026-05-01", until_further_notice: true, }),});const plan = await res.json();import requests
res = requests.post( "https://api.elasticpay.co/api/v1/payment_plans", headers={ "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, json={ "customer_id": "cus_0abc123def456ghi789jkl012mno", "name": "Monthly gym membership", "frequency_type": "monthly", "recurring_amount_cents": 4900, "currency": "AUD", "start_date": "2026-05-01", "until_further_notice": True, },)plan = res.json()<?phpuse GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.elasticpay.co', 'headers' => [ 'Authorization' => 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json', ],]);
$res = $client->post('/api/v1/payment_plans', [ 'json' => [ 'customer_id' => 'cus_0abc123def456ghi789jkl012mno', 'name' => 'Monthly gym membership', 'frequency_type' => 'monthly', 'recurring_amount_cents' => 4900, 'currency' => 'AUD', 'start_date' => '2026-05-01', 'until_further_notice' => true, ],]);$plan = json_decode($res->getBody(), true);require 'faraday'require 'json'
conn = Faraday.new('https://api.elasticpay.co') do |f| f.request :json f.response :jsonendconn.headers['Authorization'] = 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'conn.headers['Content-Type'] = 'application/json'
res = conn.post('/api/v1/payment_plans') do |req| req.body = { customer_id: 'cus_0abc123def456ghi789jkl012mno', name: 'Monthly gym membership', frequency_type: 'monthly', recurring_amount_cents: 4900, currency: 'AUD', start_date: '2026-05-01', until_further_notice: true }endplan = res.bodyusing System.Net.Http;using System.Net.Http.Headers;using System.Net.Http.Json;
using var http = new HttpClient { BaseAddress = new Uri("https://api.elasticpay.co") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
var res = await http.PostAsJsonAsync("/api/v1/payment_plans", new { customer_id = "cus_0abc123def456ghi789jkl012mno", name = "Monthly gym membership", frequency_type = "monthly", recurring_amount_cents = 4900, currency = "AUD", start_date = "2026-05-01", until_further_notice = true,});var plan = await res.Content.ReadFromJsonAsync<JsonElement>();Response
{ "id": "pp_0abc123def456ghi789jkl012mno", "customer_id": "cus_0abc123def456ghi789jkl012mno", "name": "Monthly gym membership", "state": "pending", "frequency_type": "monthly", "frequency_period": 1, "recurring_amount_cents": 4900, "currency": "AUD", "start_date": "2026-05-01", "until_further_notice": true, "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-01T10:00:00Z"}Create parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Customer (cus_xxx) who will be charged |
name | string | Yes | Human-readable label |
frequency_type | string | Yes | weekly or monthly |
frequency_period | integer | No | Interval multiplier (default 1). 2 + weekly = fortnightly |
recurring_amount_cents | integer | Yes | Amount per scheduled payment in the smallest currency unit |
first_amount_cents | integer | No | Different amount for the first payment only |
total_amount_cents | integer | No | Cap total collections; plan closes when reached |
until_further_notice | boolean | No | true for open-ended plans with no total cap |
currency | string | Yes | ISO 4217 currency code (e.g. AUD, USD) |
start_date | string | Yes | ISO date (YYYY-MM-DD) of the first payment |
first_date | string | No | Override the first payment date (defaults to start_date) |
payment_instrument_id | string | No | Saved payment method (pm_xxx) to use. Falls back to customer default |
failure_behaviour | string | No | Failure handling mode (e.g. stop, retry). See Handling Failures |
Either total_amount_cents or until_further_notice: true must be set.
Fetch a payment plan
curl https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"const res = await fetch( "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno", { headers: { "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } });const plan = await res.json();import requests
res = requests.get( "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno", headers={"Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},)plan = res.json()<?phpuse GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.elasticpay.co', 'headers' => [ 'Authorization' => 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json', ],]);
$res = $client->get('/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno');$plan = json_decode($res->getBody(), true);require 'faraday'require 'json'
conn = Faraday.new('https://api.elasticpay.co') do |f| f.request :json f.response :jsonendconn.headers['Authorization'] = 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'conn.headers['Content-Type'] = 'application/json'
res = conn.get('/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno')plan = res.bodyusing System.Net.Http;using System.Net.Http.Headers;using System.Net.Http.Json;
using var http = new HttpClient { BaseAddress = new Uri("https://api.elasticpay.co") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
var plan = await http.GetFromJsonAsync<JsonElement>( "/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno");List payment plans
curl "https://api.elasticpay.co/api/v1/payment_plans?customer_id=cus_0abc123def456" \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"const res = await fetch( "https://api.elasticpay.co/api/v1/payment_plans?customer_id=cus_0abc123def456", { headers: { "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } });const { data, has_more } = await res.json();import requests
res = requests.get( "https://api.elasticpay.co/api/v1/payment_plans", headers={"Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}, params={"customer_id": "cus_0abc123def456"},)data = res.json()<?phpuse GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.elasticpay.co', 'headers' => [ 'Authorization' => 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json', ],]);
$res = $client->get('/api/v1/payment_plans', [ 'query' => ['customer_id' => 'cus_0abc123def456'],]);$data = json_decode($res->getBody(), true);require 'faraday'require 'json'
conn = Faraday.new('https://api.elasticpay.co') do |f| f.request :json f.response :jsonendconn.headers['Authorization'] = 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'conn.headers['Content-Type'] = 'application/json'
res = conn.get('/api/v1/payment_plans') do |req| req.params['customer_id'] = 'cus_0abc123def456'enddata = res.bodyusing System.Net.Http;using System.Net.Http.Headers;using System.Net.Http.Json;
using var http = new HttpClient { BaseAddress = new Uri("https://api.elasticpay.co") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
var data = await http.GetFromJsonAsync<JsonElement>( "/api/v1/payment_plans?customer_id=cus_0abc123def456");Response shape
{ "data": [], "has_more": false}Supported query parameters:
| Parameter | Description |
|---|---|
customer_id | Filter plans for a specific customer |
state | Filter by state (pending, active, suspended, deactivated, closed) |
limit | Number of results per page (default 20, max 100) |
starting_after | Cursor: return results after this plan ID |
Update a payment plan
Use PUT to update mutable fields on a plan. Not all fields can be changed after the plan is active (e.g. frequency_type, start_date). The API will return a validation error if a field cannot be changed in the plan’s current state.
curl -X PUT https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"recurring_amount_cents": 5900}'const res = await fetch( "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno", { method: "PUT", headers: { "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recurring_amount_cents: 5900 }), });const updated = await res.json();import requests
res = requests.put( "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno", headers={ "Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, json={"recurring_amount_cents": 5900},)updated = res.json()<?phpuse GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.elasticpay.co', 'headers' => [ 'Authorization' => 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json', ],]);
$res = $client->put('/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno', [ 'json' => ['recurring_amount_cents' => 5900],]);$updated = json_decode($res->getBody(), true);require 'faraday'require 'json'
conn = Faraday.new('https://api.elasticpay.co') do |f| f.request :json f.response :jsonendconn.headers['Authorization'] = 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'conn.headers['Content-Type'] = 'application/json'
res = conn.put('/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno') do |req| req.body = { recurring_amount_cents: 5900 }endupdated = res.bodyusing System.Net.Http;using System.Net.Http.Headers;using System.Net.Http.Json;
using var http = new HttpClient { BaseAddress = new Uri("https://api.elasticpay.co") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
var res = await http.PutAsJsonAsync( "/api/v1/payment_plans/pp_0abc123def456ghi789jkl012mno", new { recurring_amount_cents = 5900 });var updated = await res.Content.ReadFromJsonAsync<JsonElement>();State transitions
Payment plans move through states via dedicated transition endpoints.
# Activate (pending -> active)curl -X POST https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123/activate \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Suspend (active -> suspended)curl -X POST https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123/suspend \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Resume (suspended -> active)curl -X POST https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123/resume \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Deactivate (active -> deactivated)curl -X POST https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123/deactivate \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Close (any -> closed)curl -X POST https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123/close \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"const AUTH = "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";const BASE = "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123";
async function transition(action) { const res = await fetch(`${BASE}/${action}`, { method: "POST", headers: { "Authorization": AUTH }, }); return res.json();}
await transition("activate"); // pending -> activeawait transition("suspend"); // active -> suspendedawait transition("resume"); // suspended -> activeawait transition("deactivate"); // active -> deactivatedawait transition("close"); // any -> closedimport requests
AUTH = {"Authorization": "Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}BASE = "https://api.elasticpay.co/api/v1/payment_plans/pp_0abc123"
def transition(action): return requests.post(f"{BASE}/{action}", headers=AUTH).json()
transition("activate") # pending -> activetransition("suspend") # active -> suspendedtransition("resume") # suspended -> activetransition("deactivate") # active -> deactivatedtransition("close") # any -> closed<?phpuse GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.elasticpay.co', 'headers' => [ 'Authorization' => 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json', ],]);
$planId = 'pp_0abc123';foreach (['activate', 'suspend', 'resume', 'deactivate', 'close'] as $action) { $client->post("/api/v1/payment_plans/{$planId}/{$action}");}require 'faraday'require 'json'
conn = Faraday.new('https://api.elasticpay.co') do |f| f.request :json f.response :jsonendconn.headers['Authorization'] = 'Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'conn.headers['Content-Type'] = 'application/json'
plan_id = 'pp_0abc123'%w[activate suspend resume deactivate close].each do |action| conn.post("/api/v1/payment_plans/#{plan_id}/#{action}")endusing System.Net.Http;using System.Net.Http.Headers;using System.Net.Http.Json;
using var http = new HttpClient { BaseAddress = new Uri("https://api.elasticpay.co") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
const string planId = "pp_0abc123";foreach (var action in new[] { "activate", "suspend", "resume", "deactivate", "close" }){ await http.PostAsync($"/api/v1/payment_plans/{planId}/{action}", null);}Valid transitions
| From | To | Endpoint |
|---|---|---|
pending | active | /activate |
active | suspended | /suspend |
active | deactivated | /deactivate |
suspended | active | /resume |
active, suspended, pending | closed | /close |
Webhook events
Subscribe to these events to monitor plan activity:
| Event | When |
|---|---|
payment_plan.activated | Plan transitions to active |
payment_plan.suspended | Plan is suspended |
payment_plan.deactivated | Plan ends |
payment_plan.closed | Plan is closed |
payment_intent.succeeded | A scheduled payment is collected |
payment_intent.failed | A scheduled payment fails |
Required permissions
| Operation | Required scope |
|---|---|
| Create / update / state transitions | payment_plans:write |
| Fetch / list | payment_plans:read |
Secret keys (sk_xxx) have both scopes by default.