Pause and resume a subscription
A pause suspends billing on an active subscription without ending it. The customer keeps their subscription record, payment method, and history; Frame stops generating invoices until the subscription resumes. Use it for the cases cancel is too blunt for: a customer taking a break, a seasonal business going dark for the off-season, a payment dispute you want to freeze billing behind.
Pause is reversible — that's the point. If the relationship is over, cancel instead.
Pause an active subscription
POST /v1/subscriptions/<id>/pause transitions an active subscription to paused immediately:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions/<subscription_id>/pause \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json'
With no request body, the pause is open-ended: the subscription stays paused until you explicitly resume it. On the response, status is paused, paused_at records when the pause began, and resume_at is null.
Only active subscriptions can pause. A subscription in past_due, unpaid, or a terminal state has to be recovered or ended through its own path first.
Schedule an automatic resume
Pass resume_at to have Frame resume the subscription on a date, no second call required:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions/<subscription_id>/pause \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"resume_at": "2026-08-09T00:00:00Z"
}'
resume_at— ISO 8601 timestamp. The subscription returns toactiveautomatically at this time.
Prefer a scheduled resume when the pause has a known end ("skip next month," "back after the summer"). An open-ended pause with no resume_at is easy to lose track of — nothing in Frame escalates or expires it.
Resume manually
POST /v1/subscriptions/<id>/resume returns a paused subscription to active immediately:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions/<subscription_id>/resume \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json'
Manual resume works whether or not the pause had a scheduled resume_at. Resuming before the scheduled date discards the schedule — the subscription is active now, and paused_at / resume_at clear on the record.
A subscription can pause and resume repeatedly over its life. Each pause is recorded as a closed period on the subscription's history, so a previously-paused subscription pauses again without ceremony.
Find paused subscriptions
The list endpoint filters on the paused status:
curl --request GET \
--url "https://api.framepayments.com/v1/subscriptions?status=paused" \
--header "Authorization: Bearer $FRAME_SECRET_KEY"
Each result carries paused_at and resume_at, so one call distinguishes scheduled pauses (resume date set) from open-ended ones (resume_at: null) — the open-ended ones are the list to review for pauses that should have ended.
To inspect a single subscription's pause state, GET /v1/subscriptions/<id> returns the same fields.
Cancel from the paused state
Cancel doesn't require resuming first. POST /v1/subscriptions/<id>/cancel works from paused the same as from active:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions/<subscription_id>/cancel \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json'
The subscription transitions straight to canceled — a terminal state. Use this when a paused customer decides they're not coming back.
Webhook signal
Pause and resume transitions fire webhooks, prefixed by owner type like the rest of the subscription lifecycle:
customer.subscription.paused/customer.subscription.resumed— customer-owned subscriptionssubscription.paused/subscription.resumed— account-owned subscriptions
The paused event's payload carries the pause details on the subscription object: paused_at and resume_at as Unix timestamps, plus a nested pause_period object describing the open pause:
{
"type": "customer.subscription.paused",
"data": {
"object": {
"id": "sub_...",
"object": "subscription",
"status": "paused",
"paused_at": 1720000000,
"resume_at": 1721200000,
"pause_period": {
"id": "spp_...",
"object": "subscription_pause_period",
"paused_at": 1720000000,
"scheduled_resume_at": 1721200000,
"ended_at": null,
"end_reason": null
}
}
}
}
For an open-ended pause, resume_at and pause_period.scheduled_resume_at are null.
The resumed event describes the subscription after the resume: status is active, and paused_at, resume_at, and pause_period are all null — the pause period is closed by the time the event fires. Treat resumed as a "billing is back on" signal, not a source of pause history. If you need the details of a pause after it ends, record them from the paused event when it arrives.
Gotchas
Symptom: you canceled a subscription to stop billing temporarily and now can't reactivate it. Why: canceled is terminal — there's no un-cancel. Fix: pause is the reversible stop. To recover a canceled customer, create a new subscription against the same customer and payment method.
Symptom: a customer's "one month off" pause is still dark three months later. Why: an open-ended pause never escalates or expires on its own. Fix: pass resume_at when the pause has a known end, and periodically sweep GET /v1/subscriptions?status=paused for records with resume_at: null.
Symptom: a customer scheduled to auto-resume wants to come back early, and you're not sure the schedule allows it. Why: nothing blocks it — manual resume takes precedence over a scheduled resume_at. Fix: call POST /v1/subscriptions/<id>/resume; the subscription is active immediately and the scheduled date is discarded.
Symptom: your resumed webhook handler reads paused_at to compute how long the customer was paused, and it's always null. Why: the resumed payload describes the subscription after the resume — the pause fields are cleared and the pause period is closed before the event fires. Fix: capture paused_at from the paused event when it arrives, or diff the timestamps in your own records.
Symptom: the resume_at you sent doesn't match the resume_at in the webhook payload. Why: the API request takes an ISO 8601 string, but webhook payloads carry Unix epoch integers — same instant, different encoding. Fix: compare as instants, not strings.
Next steps
- Subscriptions concept for the full lifecycle state machine
- Invoices concept for how billing cycles generate invoices
- Build subscriptions for creation, webhooks, and cancellation