Subscription Billing
Set up recurring billing with products, plans, trials, and renewal events.
What you’ll build
A subscription that bills the same customer on a recurring schedule (e.g. $29.99 every month). EPD handles the renewal, retry, and dunning for you. Your job is to react to lifecycle events.
Use this recipe for: SaaS plans, memberships, recurring services, scheduled top-ups.
The model
EPD separates the thing being sold (a product) from the price and cadence it is sold at (a plan), then ties a customer to a plan via a subscription.
Product ──► Plan ──► Subscription
(catalog) (pricing) (one customer's contract)
Prerequisites
- A customer with at least one attached payment method (see one-time payment recipe for the steps).
- A registered webhook endpoint for
subscription.*events. - A plan created in the Merchant Portal (Plans → New plan). Plans are read-only via the API today; create them in the portal and reference them by
idhere.
Steps
Open the Merchant Portal, create the product (e.g. “Pro Plan”), then create a plan against it with the price (amount in cents) and billing interval (month/day). Copy the resulting plan_id — you’ll pass it to the API in the next step.
The public API exposes plans as read-only today (GET /v1/plans, GET /v1/plans/:id), so creation must happen in the portal.
Bind the customer, the plan, and the payment method together. The billing_cycle block tells EPD how often to charge.
curl https://api.epd.com/v1/subscriptions \
-H "Authorization: Bearer $EPD_KEY" \
-H "epd-version: 2026-02-11" \
-H "X-EPD-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"plan_id": "6ba7b814-9dad-11d1-80b4-00c04fd430d0",
"payment_method_id": "6ba7b815-9dad-11d1-80b4-00c04fd430c8",
"billing_cycle": {
"interval": "month",
"interval_count": 1,
"anchor_day": "signup_date"
}
}'
The first charge runs immediately. anchor_day controls when each renewal hits: leave it as "signup_date" (the default) to bill on whatever day they signed up, or set it to a number from 1 to 31 — for example 15 — to bill on a fixed day every month.
Each renewal cycle fires events you should handle in your app — see the table below.
Free trials are not currently supported on the API. If you need a trial, charge a $0 plan in the portal, or implement the trial period in your own application logic and switch to the paid plan when it ends.
Viewing subscriptions in the portal
In the Merchant Portal, choose Subscriptions from the left sidebar to open the list of every subscription on the account. Click a row and a right-side drawer slides in with the customer, current plan, billing schedule, and shipping info; the edit icon in that drawer opens a full edit page where you can swap the payment method or shipping address.
Lifecycle events
| Event | When it fires | What you typically do |
|---|---|---|
subscription.created | Subscription activated (first charge succeeded) | Provision access in your product |
subscription.charged | Each successful renewal charge | Extend access through the next period |
subscription.updated | Payment method, shipping, or billing cycle changed | Update your local copy |
subscription.billing.updated | Billing cycle (interval/anchor_day) changed | Reflect the new schedule in your UI |
subscription.canceled | Subscription canceled (always immediate) | Revoke access |
Each renewal creates a regular order behind the scenes. To detect a failed renewal, listen for order.failed — there is no dedicated subscription.payment_failed event today.
Canceling a subscription
Cancellation is always immediate — there is no at_period_end flag today. Use DELETE /v1/subscriptions/:id with no body:
curl -X DELETE https://api.epd.com/v1/subscriptions/$SUBSCRIPTION_ID \
-H "Authorization: Bearer $EPD_KEY" \
-H "epd-version: 2026-02-11" \
-H "X-EPD-Idempotency-Key: $(uuidgen)"
If you want the customer to keep access until the end of the period they’ve paid for, hold off on calling cancel and run it from your own scheduler (a cron job, a queued task, or whatever your app already uses) on the period-end date.
Pause and resume
POST /v1/subscriptions/:id/pause and POST /v1/subscriptions/:id/resume exist but currently return HTTP 501 Not Implemented with error.code = not_implemented. The feature is on the roadmap. To pause-and-resume today, cancel the subscription and create a fresh one when the customer comes back.