Subscription Example (with autopay)
End-to-end example of a subscription with autopay, from order creation to cancellation.
This guide walks through a complete subscription with autopay — from creating an open-ended $49.99/month order to cancellation. You'll see every API call, webhook, and email along the way.
PrerequisitesThis builds on concepts from Orders and Pay Schedules. Read those first if you haven't already.
The scenario
Alex Chen signs up for a $49.99/month gym membership with autopay. There's no end date — the subscription runs indefinitely until cancelled:
- Month 1 (Apr 10): $49.99 charged at start
- Month 2 (May 10): $49.99 autopay
- Month 3 (Jun 10): $49.99 autopay
- Month 4 (Jul 10): $49.99 autopay
- ...and so on until you cancel the subscription
Subscriptions vs. payment plansA subscription has no total
amount— therecurringAmountis charged every period indefinitely. A payment plan has a fixedamountand ends when the balance reaches zero. See Pay Schedules for more on the difference, or the comparison table at the end of this page.
Subscription status lifecycle
Subscriptions use their own set of statuses, separate from payment plan statuses:
stateDiagram-v2
[*] --> SUBSCRIPTION_NOT_STARTED : Order created with paySchedule
SUBSCRIPTION_NOT_STARTED --> SUBSCRIPTION_ACTIVE : Schedule started
SUBSCRIPTION_ACTIVE --> SUBSCRIPTION_PAST_DUE : Payment missed (day after due date)
SUBSCRIPTION_PAST_DUE --> SUBSCRIPTION_ACTIVE : Past due amount paid
SUBSCRIPTION_ACTIVE --> SUBSCRIPTION_CANCELLED : Schedule cancelled
SUBSCRIPTION_CANCELLED --> [*]
| Status | Meaning |
|---|---|
SUBSCRIPTION_NOT_STARTED | Order created with a pay schedule, but the schedule hasn't been started yet |
SUBSCRIPTION_ACTIVE | Schedule is running and all payments are current |
SUBSCRIPTION_PAST_DUE | A billing period was missed — the customer owes for a past period |
SUBSCRIPTION_CANCELLED | Schedule was cancelled (either by you or the customer) |
No direct path from past due to cancelledYou cannot cancel a subscription that has an outstanding past due balance. The customer must pay what they owe before the schedule can be cancelled. The path is always
SUBSCRIPTION_PAST_DUE→SUBSCRIPTION_ACTIVE→SUBSCRIPTION_CANCELLED.
1. Create the order
Create an order without an amount field — this tells the API it's a subscription. Include a paySchedule with the monthly charge and enable autopay.
POST /n1/merchant/{merchantId}/order/{orderId?}
{
"description": "Premium gym membership - monthly",
"customers": [
{
"firstName": "Alex",
"lastName": "Chen",
"email": "[email protected]"
}
],
"paySchedule": {
"recurringAmount": 49.99,
"frequency": "MONTHLY",
"autopay": true
}
}Response 201 Created:
{
"success": true,
"statusCode": 201,
"data": {
"id": "GYM1-AX7K",
"merchantId": "Z70B874W63DW",
"description": "Premium gym membership - monthly",
"currency": "USD",
"type": "SUBSCRIPTION",
"status": "SUBSCRIPTION_NOT_STARTED",
"paySchedule": {
"recurringAmount": 49.99,
"currency": "USD",
"frequency": "MONTHLY",
"isActive": false,
"autopay": true,
"reminderBeforeDueDays": [7, 3],
"retryAfterDueDays": [1, 3, 7],
"sendSms": false,
"sendEmail": true
},
"customers": [
{
"firstName": "Alex",
"lastName": "Chen",
"email": "[email protected]",
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:00:00.000+00:00"
}
],
"payments": [],
"invoiceEmailSends": [],
"invoiceSmsSends": [],
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:00:00.000+00:00",
"invoiceUrl": "https://gateway.prahsys.com/order/019d7a00-0000-7000-8000-000000000002/pay-schedule/invoice?expires=1783886400&signature=e5f6a7b8..."
}
}A few things to note:
typeisSUBSCRIPTIONbecausepayScheduleis present butamountis omitted.- There is no
amountorremainingBalancefield in the response — subscriptions don't have a fixed total. isActiveisfalse— creating the order does not start billing. The schedule stays inactive until you explicitly start it.- Default
reminderBeforeDueDaysandretryAfterDueDaysforMONTHLYfrequency were applied automatically.
2. Start the subscription
There are two ways to kick things off. Pick whichever fits your integration.
Option A: Customer starts via the invoice page
Send the invoice and let the customer handle the rest.
Step 1: Send the invoice.
POST /n1/merchant/{merchantId}/order/GYM1-AX7K/send
{
"sendToCustomerEmails": true
}Step 2: Customer receives the email. Alex gets a welcome email with a link to the invoice page. Preview the welcome email.
Direct linkYou don't have to send the email through our API. The
invoiceUrlin the order response points to the same invoice page — you can give that URL to the customer directly, or link to it from your own app.
Step 3: Customer completes setup. On the invoice page, Alex adds a card (which gets tokenized automatically) and makes the first $49.99 payment. This activates the schedule and the same webhooks and emails are sent as in Option B below.
Option B: Merchant starts via the API
Step 1: Attach a billing token. If you already have a pay token from tokenization, attach it to the pay schedule:
PUT /n1/merchant/{merchantId}/order/GYM1-AX7K
{
"paySchedule": {
"billing": {
"token": "tok_mG7kP2xR9vNq4242"
}
}
}
Using a session insteadYou can skip this step and provide a
session.idin the start request instead. The system will tokenize the session's payment method automatically. See the Pay Session docs for details.
Step 2: Start the schedule.
POST /n1/merchant/{merchantId}/order/GYM1-AX7K/pay-schedule/start
{
"payOnStart": true
}With payOnStart: true, the first $49.99 is charged immediately.
Response 201 Created:
{
"success": true,
"statusCode": 201,
"message": "Pay schedule started successfully. First payment has been processed.",
"data": {
"id": "GYM1-AX7K",
"merchantId": "Z70B874W63DW",
"description": "Premium gym membership - monthly",
"currency": "USD",
"type": "SUBSCRIPTION",
"status": "SUBSCRIPTION_ACTIVE",
"paySchedule": {
"recurringAmount": 49.99,
"currency": "USD",
"frequency": "MONTHLY",
"isActive": true,
"autopay": true,
"startDate": "2026-04-10",
"currentDueDate": "2026-05-10",
"nextReminderDate": "2026-05-03",
"billing": {
"card": { "numberMasked": "xxxxxxxxxxxx4242" },
"token": "tok_mG7kP2xR9vNq4242",
"method": "CARD"
},
"reminderBeforeDueDays": [7, 3],
"retryAfterDueDays": [1, 3, 7],
"sendSms": false,
"sendEmail": true
},
"customers": [
{
"firstName": "Alex",
"lastName": "Chen",
"email": "[email protected]",
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:00:00.000+00:00"
}
],
"payments": [
{
"id": "AUTOPAY-Z70B874W63DW-a1b2c3d4e5f6",
"merchantId": "Z70B874W63DW",
"orderId": "GYM1-AX7K",
"amount": 49.99,
"currency": "USD",
"description": "Autopay payment for order GYM1-AX7K",
"status": "CAPTURED",
"billing": {
"card": { "numberMasked": "xxxxxxxxxxxx4242" },
"token": "tok_mG7kP2xR9vNq4242",
"method": "CARD"
},
"creationTime": "2026-04-10T12:02:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:02:00.000+00:00"
}
],
"invoiceEmailSends": [],
"invoiceSmsSends": [],
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:02:00.000+00:00",
"invoiceUrl": "https://gateway.prahsys.com/order/019d7a00-0000-7000-8000-000000000002/pay-schedule/invoice?expires=1783886400&signature=e5f6a7b8..."
}
}Step 3: Two webhooks fire.
orders.pay_schedule.started — the schedule is now active:
{
"eventType": "orders.pay_schedule.started",
"payload": {
"data": {
"id": "GYM1-AX7K",
"type": "SUBSCRIPTION",
"status": "SUBSCRIPTION_ACTIVE",
"paySchedule": {
"isActive": true,
"startDate": "2026-04-10",
"currentDueDate": "2026-05-10"
/* ...full pay schedule object */
}
/* ...full order object */
}
}
}orders.status_changed — the order moved from SUBSCRIPTION_NOT_STARTED to SUBSCRIPTION_ACTIVE:
{
"eventType": "orders.status_changed",
"payload": {
"data": { /* full order object */ },
"previousStatus": "SUBSCRIPTION_NOT_STARTED",
"newStatus": "SUBSCRIPTION_ACTIVE"
}
}Step 4: "Started" email sent to Alex. Preview the started email.
3. What happens each month
With the subscription started and autopay enabled, everything runs on autopilot. Here's the timeline — note that unlike a payment plan, there is no end date:
gantt
title $49.99/month Subscription — Autopay (open-ended)
dateFormat YYYY-MM-DD
axisFormat %b %d
section Month 1
Period 1 :done, p1, 2026-04-10, 30d
First payment $49.99 :milestone, done, m1, 2026-04-10, 0d
section Month 2
Reminder (7 days before) :milestone, r2a, 2026-05-03, 0d
Reminder (3 days before) :milestone, r2b, 2026-05-07, 0d
Period 2 :done, p2, 2026-05-10, 30d
Autopay charges $49.99 :milestone, done, m2, 2026-05-10, 0d
section Month 3
Reminder (7 days before) :milestone, r3a, 2026-06-03, 0d
Reminder (3 days before) :milestone, r3b, 2026-06-07, 0d
Period 3 :done, p3, 2026-06-10, 30d
Autopay charges $49.99 :milestone, done, m3, 2026-06-10, 0d
section Month 4
Reminder (7 days before) :milestone, r4a, 2026-07-03, 0d
Reminder (3 days before) :milestone, r4b, 2026-07-07, 0d
Period 4 :done, p4, 2026-07-10, 30d
Autopay charges $49.99 :milestone, done, m4, 2026-07-10, 0d
section Continues...
Period 5, 6, 7 ... :active, p5, 2026-08-10, 30d
The monthly cycle
Each billing period follows the same pattern:
sequenceDiagram
participant System as Prahsys System
participant Gateway as Payment Gateway
participant Server as Your Server (webhook)
participant Customer as Customer (email)
Note over System: 7 days before due date
System->>Customer: Autopay reminder email
Note over System: 3 days before due date
System->>Customer: Autopay reminder email
Note over System: Due date
System->>Gateway: Charge stored token ($49.99)
Gateway-->>System: Payment successful
System->>Server: orders.pay_schedule.period.fulfilled
System->>Customer: Payment receipt email
- 7 days before due: Autopay reminder email. Preview.
- 3 days before due: Second reminder email.
- On the due date: Autopay charges the stored card. The
orders.pay_schedule.period.fulfilledwebhook fires and a payment receipt is emailed to Alex.
Configurable remindersThe 7-day and 3-day reminders are the
MONTHLYdefaults. You can customize the schedule by settingreminderBeforeDueDayswhen creating or updating the pay schedule — for example,[10, 5, 1]to send reminders 10, 5, and 1 day before each due date.
Running billing
Unlike a payment plan, there is no remaining balance to track. The same charge repeats every period:
| Month | Date | Charged | Status | Webhooks Fired |
|---|---|---|---|---|
| 1 | Apr 10 | $49.99 | SUBSCRIPTION_ACTIVE | started, period.fulfilled, status_changed |
| 2 | May 10 | $49.99 | SUBSCRIPTION_ACTIVE | period.fulfilled |
| 3 | Jun 10 | $49.99 | SUBSCRIPTION_ACTIVE | period.fulfilled |
| 4 | Jul 10 | $49.99 | SUBSCRIPTION_ACTIVE | period.fulfilled |
| ... | ... | $49.99 | SUBSCRIPTION_ACTIVE | period.fulfilled |
Status change webhooksThe
orders.status_changedwebhook only fires when the status actually transitions. Month 1 triggers it (SUBSCRIPTION_NOT_STARTED→SUBSCRIPTION_ACTIVE). Subsequent months stay atSUBSCRIPTION_ACTIVE, so no status webhook fires — onlyperiod.fulfilled.
No final paymentUnlike payment plans, subscriptions never auto-adjust a final payment or deactivate on their own. The same
recurringAmountis charged every period until you explicitly cancel the schedule.
4. Cancelling the subscription
When you're ready to end the subscription — whether the customer requests it or you decide to stop billing — call the cancel endpoint. Subscriptions run indefinitely, so this is the only way to stop them.
POST /n1/merchant/{merchantId}/order/GYM1-AX7K/pay-schedule/cancel
No request body is needed.
Restrictions
- The pay schedule must be active to cancel it.
- You cannot cancel a pay schedule that has an outstanding past due balance. The customer must pay what they owe for past periods before the schedule can be cancelled.
sequenceDiagram
participant Merchant as Your Server
participant System as Prahsys System
participant Server as Your Server (webhook)
participant Customer as Customer (email)
Merchant->>System: POST .../pay-schedule/cancel
System->>System: Deactivate schedule (isActive = false)
System->>System: Clear currentDueDate, nextReminderDate, nextRetryDate
System->>System: status: SUBSCRIPTION_ACTIVE → SUBSCRIPTION_CANCELLED
System-->>Merchant: 200 OK (updated order)
System->>Server: orders.pay_schedule.cancelled
System->>Server: orders.status_changed (SUBSCRIPTION_ACTIVE → SUBSCRIPTION_CANCELLED)
System->>Customer: Subscription cancelled email
Response 200 OK:
{
"success": true,
"statusCode": 200,
"message": "Pay schedule cancelled successfully.",
"data": {
"id": "GYM1-AX7K",
"merchantId": "Z70B874W63DW",
"description": "Premium gym membership - monthly",
"currency": "USD",
"type": "SUBSCRIPTION",
"status": "SUBSCRIPTION_CANCELLED",
"paySchedule": {
"recurringAmount": 49.99,
"currency": "USD",
"frequency": "MONTHLY",
"isActive": false,
"autopay": true,
"startDate": "2026-04-10",
"currentDueDate": null,
"nextReminderDate": null,
"nextRetryDate": null,
"billing": {
"card": { "numberMasked": "xxxxxxxxxxxx4242" },
"token": "tok_mG7kP2xR9vNq4242",
"method": "CARD"
},
"reminderBeforeDueDays": [7, 3],
"retryAfterDueDays": [1, 3, 7],
"sendSms": false,
"sendEmail": true
},
"customers": [
{
"firstName": "Alex",
"lastName": "Chen",
"email": "[email protected]",
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:00:00.000+00:00"
}
],
"payments": [
{
"id": "AUTOPAY-Z70B874W63DW-a1b2c3d4e5f6",
"merchantId": "Z70B874W63DW",
"orderId": "GYM1-AX7K",
"amount": 49.99,
"currency": "USD",
"description": "Autopay payment for order GYM1-AX7K",
"status": "CAPTURED",
"creationTime": "2026-04-10T12:02:00.000+00:00",
"lastUpdatedTime": "2026-04-10T12:02:00.000+00:00"
},
{
"id": "AUTOPAY-Z70B874W63DW-g7h8i9j0k1l2",
"merchantId": "Z70B874W63DW",
"orderId": "GYM1-AX7K",
"amount": 49.99,
"currency": "USD",
"description": "Autopay payment for order GYM1-AX7K",
"status": "CAPTURED",
"creationTime": "2026-05-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-05-10T12:00:00.000+00:00"
},
{
"id": "AUTOPAY-Z70B874W63DW-m3n4o5p6q7r8",
"merchantId": "Z70B874W63DW",
"orderId": "GYM1-AX7K",
"amount": 49.99,
"currency": "USD",
"description": "Autopay payment for order GYM1-AX7K",
"status": "CAPTURED",
"creationTime": "2026-06-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-06-10T12:00:00.000+00:00"
}
],
"invoiceEmailSends": [],
"invoiceSmsSends": [],
"creationTime": "2026-04-10T12:00:00.000+00:00",
"lastUpdatedTime": "2026-07-05T14:30:00.000+00:00",
"invoiceUrl": "https://gateway.prahsys.com/order/019d7a00-0000-7000-8000-000000000002/pay-schedule/invoice?expires=1783886400&signature=e5f6a7b8..."
}
}Two webhooks fire when a subscription is cancelled:
orders.pay_schedule.cancelled:
{
"eventType": "orders.pay_schedule.cancelled",
"payload": {
"data": {
"id": "GYM1-AX7K",
"status": "SUBSCRIPTION_CANCELLED",
"paySchedule": {
"isActive": false,
"currentDueDate": null,
"nextReminderDate": null,
"nextRetryDate": null
}
/* ...full order object */
}
}
}orders.status_changed:
{
"eventType": "orders.status_changed",
"payload": {
"data": { /* full order object — status: "SUBSCRIPTION_CANCELLED", isActive: false */ },
"previousStatus": "SUBSCRIPTION_ACTIVE",
"newStatus": "SUBSCRIPTION_CANCELLED"
}
}Alex receives a cancellation email. Preview the cancelled email.
Cancellation is immediateOnce cancelled, no further charges or reminders will be sent. The billing token remains on file but is never used again. If the customer wants to resubscribe, create a new order.
5. What if autopay fails?
If the stored card is declined on a due date, the system retries automatically on the days configured in retryAfterDueDays. For MONTHLY, the default is days 1, 3, and 7 after the due date. The day after the due date, the order status changes to SUBSCRIPTION_PAST_DUE.
sequenceDiagram
participant System as Prahsys System
participant Gateway as Payment Gateway
participant Server as Your Server (webhook)
participant Customer as Customer (email)
Note over System: Jun 10 — Due date
System->>Gateway: Charge $49.99
Gateway-->>System: DECLINED (card expired)
System->>Server: orders.pay_schedule.autopay.failed
System->>Customer: Autopay failed email
Note over System: Jun 11 — Retry day 1
System->>System: status: SUBSCRIPTION_ACTIVE → SUBSCRIPTION_PAST_DUE
System->>Server: orders.status_changed (SUBSCRIPTION_ACTIVE → SUBSCRIPTION_PAST_DUE)
System->>Gateway: Retry charge $49.99
Gateway-->>System: DECLINED
System->>Server: orders.pay_schedule.autopay.failed
System->>Customer: Autopay failed email
Note over System: Jun 13 — Retry day 3
System->>Gateway: Retry charge $49.99
Gateway-->>System: Payment successful
System->>System: status: SUBSCRIPTION_PAST_DUE → SUBSCRIPTION_ACTIVE
System->>Server: orders.pay_schedule.period.fulfilled
System->>Server: orders.status_changed (SUBSCRIPTION_PAST_DUE → SUBSCRIPTION_ACTIVE)
System->>Customer: Payment receipt email
Each failed attempt fires an orders.pay_schedule.autopay.failed webhook and sends a failure email to the customer. The email includes a link to the invoice page where they can update their payment method. Preview the autopay failed email.
After all retries are exhaustedIf all retry attempts fail (days 1, 3, and 7 for MONTHLY), the subscription stays active and advances to the next period. The past-due amount rolls forward into the next charge. The customer can also pay manually through the invoice page at any time.
Subscription vs. payment plan — quick comparison
| Subscription | Payment Plan | |
|---|---|---|
amount field | Not set (omitted) | Required (e.g., 500.00) |
remainingBalance | Not present | Tracks balance to $0 |
| Ends when | You cancel it | Balance reaches $0 (auto-completes) |
| Status values | SUBSCRIPTION_NOT_STARTED, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAST_DUE, SUBSCRIPTION_CANCELLED | PENDING, PARTIALLY_PAID, PAST_DUE, PAID |
| Extra payments | Not allowed | Allowed (pays down balance faster) |
| Final payment auto-adjust | N/A | Yes — min(recurringAmount, remainingBalance) |
| "Completed" email | N/A | Sent when balance reaches $0 |
Email reference
No "completed" emailUnlike payment plans, subscriptions don't have a "Plan completed" email because they never reach a zero balance. They run until explicitly cancelled.
Webhook reference
For the full list of pay schedule webhook events and their payloads, see Pay Schedules — Webhooks.
Updated about 19 hours ago
