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 to active automatically 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 subscriptions
  • subscription.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