CommetCommet
GitHubDiscordStatus
Introduction

Quickstart

Create an API KeyQuickstart

Learn

Manage SubscriptionsGrant Temporary Plan AccessUpgrade and Downgrade Plans

Resources

SDK ReferenceAPI VersioningError HandlingTestingCLIExamples

Plugins

Better Auth
DocumentationKnowledge BaseBuild with AIAPI ReferenceWebhooks

Manage Subscriptions

Create, retrieve, pause, resume, cancel, and reactivate subscriptions.

Install the Commet Skill so your coding agent can implement the current subscription lifecycle and verify the result.

npx skills add commet-labs/skills --skill commet

Subscriptions connect a customer to a plan and drive checkout, invoices, feature access, usage, and renewals.

Lifecycle

The persisted statuses are:

StatusMeaning
draftCreated but not ready for billing
pending_paymentWaiting for checkout
trialingTrial access is active
activeBilling normally
past_dueRenewal failed and dunning is active
pausedAccess and renewals paused until resume
canceledBilling and subscription access ended

Create

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const createdSubscription = await commet.subscriptions.create({  customerId: "user_123",  planId: "pln_xxx",});
from commet import Commetcommet = Commet("ck_xxx")created_subscription = commet.subscriptions.create(    customer_id="user_123",    plan_id="pln_xxx",)
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()createdSubscription, err := client.Subscriptions.Create(ctx, &commet.CreateSubscriptionParams{	CustomerID: "user_123",	PlanID: func(value string) *string { return &value }("pln_xxx"),})if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.CreateSubscriptionParams;var commet = Commet.builder().apiKey("ck_xxx").build();var createdSubscription = commet.subscriptions().create(    CreateSubscriptionParams.builder("user_123").planId("pln_xxx").build());
use Commet\Commet;$commet = new Commet('ck_xxx');$createdSubscription = $commet->subscriptions->create(    customerId: 'user_123',    planId: 'pln_xxx',);

For a paid plan, redirect the customer to checkoutUrl. Free plans can activate without checkout and return checkoutUrl: null.

The normal path needs only customerId and either planCode or planId. Optional selection fields are:

FieldWhen to send it
billingIntervalThe customer selected a non-default interval
priceIdThe customer selected a concrete price variant
offerIdYour application selected an Offer directly; it overrides automatic introductory selection
promoCodeThe customer entered a Promo Code
initialSeatsYou know the initial seat quantities at creation
skipTrial or customTrialDaysYou intentionally override the configured trial

Omitting priceId preserves default price and Market resolution. Omitting offerId preserves automatic Introductory Offer selection.

A compatible pending_payment checkout may be reused. An incompatible pending selection can be replaced without duplicating a paid subscription.

Retrieve current or historical state

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const subscription = await commet.subscriptions.getActive({ customerId: "user_123" });
from commet import Commetcommet = Commet("ck_xxx")subscription = commet.subscriptions.get_active(customer_id="user_123")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()subscription, err := client.Subscriptions.GetActive(ctx, &commet.GetActiveSubscriptionParams{	CustomerID: "user_123",})if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.GetActiveSubscriptionParams;var commet = Commet.builder().apiKey("ck_xxx").build();var subscription = commet.subscriptions().getActive(    GetActiveSubscriptionParams.builder("user_123").build());
use Commet\Commet;$commet = new Commet('ck_xxx');$subscription = $commet->subscriptions->getActive(customerId: 'user_123');

getActive returns the customer's current subscription relationship or null.

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const subscription = await commet.subscriptions.get({ id: "sub_xxx" });
from commet import Commetcommet = Commet("ck_xxx")subscription = commet.subscriptions.get("sub_xxx")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()subscription, err := client.Subscriptions.Get(ctx, "sub_xxx")if err != nil {	log.Fatal(err)}
import co.commet.Commet;var commet = Commet.builder().apiKey("ck_xxx").build();var subscription = commet.subscriptions().get("sub_xxx");
use Commet\Commet;$commet = new Commet('ck_xxx');$subscription = $commet->subscriptions->get(id: 'sub_xxx');

Use get with a public subscription ID to retrieve any persisted status, including pending_payment, past_due, and canceled.

Pause and resume

A pause temporarily stops subscription access and renewals without canceling the subscription. You can pause a paid recurring subscription in active or trialing status. Free, one-time, pending-payment, past-due, and canceled subscriptions are not eligible. Resolve any scheduled cancellation, scheduled plan change, or pending plan-change checkout before creating a pause.

Choose when to pause

ModeAccess and billingWhat happens on resume
immediateAccess stops immediately. The unused portion of the current paid or trial period is preserved.Restores the remaining period without a new charge. Renewal moves to the end of that remaining time.
period_endAccess continues until the current billing or trial period ends. The next renewal is skipped.Charges a new period using the saved payment method. Access returns after successful settlement.

For example, an immediate pause with 10 paid days remaining preserves those 10 days. Resuming after a week still leaves 10 days before renewal.

While paused, the subscription does not generate renewal invoices. A period-end pause may still produce a final invoice for pending usage, overage, or seat/quota adjustments from the period that ended. It does not charge advance capacity for the next period.

Set the duration

Set durationDays to a positive integer for an automatic resume attempt, or null for an indefinite pause that requires manual resume. The duration starts when the pause becomes effective, not when a future pause is scheduled.

In the Dashboard, open the subscription detail and choose Pause. Choose when the pause starts, then select a suggested duration or enter a number of days, weeks, or calendar months. Leave the duration empty for an indefinite pause. The dialog shows the effective date and the automatic resume attempt date before you confirm. Customers can edit an existing pause, cancel a scheduled pause, and resume a paused subscription in the Customer Portal, but cannot create pauses; Commet Admin exposes them in the subscription detail. Dashboard mutations require permission to edit subscriptions.

Editing a pause derives an equivalent duration from its stored dates: weekly subscriptions prefer whole weeks; other subscriptions prefer whole calendar months, then weeks or days. Durations are always measured from the original effective date; a month ending on a shorter month uses its last day. The current pause shows its effective date and resume date. Use its actions to edit the duration, revoke a scheduled pause, or resume an effective pause.

Create a pause

This request schedules a seven-day pause at period end:

curl -X POST https://commet.co/api/v1/subscriptions/sub_xxx/pause \
  -H "x-api-key: $COMMET_API_KEY" \
  -H "commet-version: 2026-08-27" \
  -H "Content-Type: application/json" \
  -d '{"mode":"period_end","durationDays":7}'

Use "mode":"immediate" to pause now, or "durationDays":null to leave the resume date open.

Creation returns the updated subscription. A future pause has pause.status: "scheduled" while the subscription remains active or trialing. Once effective, the subscription is paused and pause.status is "active". The pause object includes mode, effectiveAt, and resumeAt; it is null when there is no current or scheduled pause.

Edit or revoke a pause

Change the duration of a scheduled or effective pause:

curl -X PATCH https://commet.co/api/v1/subscriptions/sub_xxx/pause \
  -H "x-api-key: $COMMET_API_KEY" \
  -H "commet-version: 2026-08-27" \
  -H "Content-Type: application/json" \
  -d '{"durationDays":14}'

This sets the total duration to 14 days from the original effective date; it does not add 14 days from today. Send null to make the pause indefinite. This operation does not change its mode.

Revoke a pause before it becomes effective:

curl -X DELETE https://commet.co/api/v1/subscriptions/sub_xxx/pause \
  -H "x-api-key: $COMMET_API_KEY" \
  -H "commet-version: 2026-08-27"

The subscription keeps its current period and access. An effective pause must be resumed instead.

Resume and handle payment outcomes

Use resume for a paused subscription; reactivate is a separate operation for canceled or past-due subscriptions.

curl -X POST https://commet.co/api/v1/subscriptions/sub_xxx/resume \
  -H "x-api-key: $COMMET_API_KEY" \
  -H "commet-version: 2026-08-27"

The response includes subscriptionId, invoiceId, and status. An immediate resume has no new invoice, so invoiceId is null.

ResponseMeaning
200, status: "succeeded"Resume completed. Access is restored.
200, status: "processing"The resume payment or settlement is still pending. Do not restore access based on this response alone.
402, charge_failedThe payment was declined. The subscription stays paused.
422, no_payment_methodA period-end resume needs a saved payment method.
500, internal_errorAn internal failure prevented completion; this is distinct from a declined payment.

A declined resume keeps its invoice outstanding. Automatic payment retries follow days 1, 3, 5, and 7 from the original failure and reuse that invoice. The subscription stays paused during retries. If all retries fail, it is canceled and the resume invoice becomes uncollectible. Manual resume attempts do not consume automatic retry slots.

A period-end resume uses the current base price when its new invoice is created. An outstanding resume invoice keeps its original amounts and included credits or balance on retry, even if the catalog changes again. Accepted Introductory Offers and discounts preserve their unconsumed terms; resume does not grant a new Introductory Offer or redeem a promo code again.

See the Pause API reference and Resume API reference for the complete schemas.

Keep access in sync

Use Feature Access to authorize requests against the current subscription state. Subscribe to these webhooks for asynchronous updates in your application:

  • subscription.pause_scheduled: a future pause was scheduled; access has not stopped yet.
  • subscription.pause_updated: the pause duration changed.
  • subscription.pause_revoked: the scheduled pause was removed.
  • subscription.paused: the pause became effective and subscription access stopped.
  • subscription.resumed: resume completed and subscription access returned.
  • subscription.resume_failed: resume payment failed and the subscription remains paused.

Cancel

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const subscription = await commet.subscriptions.cancel({ id: "sub_xxx" });
from commet import Commetcommet = Commet("ck_xxx")subscription = commet.subscriptions.cancel("sub_xxx")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()subscription, err := client.Subscriptions.Cancel(ctx, "sub_xxx", nil)if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.CancelSubscriptionParams;var commet = Commet.builder().apiKey("ck_xxx").build();var subscription = commet.subscriptions().cancel(    "sub_xxx",    CancelSubscriptionParams.builder().build());
use Commet\Commet;$commet = new Commet('ck_xxx');$subscription = $commet->subscriptions->cancel(id: 'sub_xxx');

A normal paid active subscription schedules cancellation at period end unless immediate: true is sent. Free, pending-payment, and past-due relationships cancel immediately. Cancellation does not erase the stored subscription balance.

Canceling a paused subscription is immediate and discards its preserved time. Pending usage, overage, or seat adjustments may still produce a final invoice. To cancel while a future pause is scheduled, revoke that pause first.

Reverse a scheduled cancellation

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const subscription = await commet.subscriptions.uncancel({ id: "sub_xxx" });
from commet import Commetcommet = Commet("ck_xxx")subscription = commet.subscriptions.uncancel("sub_xxx")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()subscription, err := client.Subscriptions.Uncancel(ctx, "sub_xxx", nil)if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.UncancelSubscriptionParams;var commet = Commet.builder().apiKey("ck_xxx").build();var subscription = commet.subscriptions().uncancel(    "sub_xxx",    UncancelSubscriptionParams.builder().build());
use Commet\Commet;$commet = new Commet('ck_xxx');$subscription = $commet->subscriptions->uncancel(id: 'sub_xxx');

uncancel works only before an end-of-period cancellation takes effect. It keeps the same subscription and current period.

Recover a past-due subscription

Reactivate retries the outstanding renewal charge and keeps the original billing relationship:

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const reactivatedSubscription = await commet.subscriptions.reactivate({ id: "sub_xxx" });
from commet import Commetcommet = Commet("ck_xxx")reactivated_subscription = commet.subscriptions.reactivate("sub_xxx")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()reactivatedSubscription, err := client.Subscriptions.Reactivate(ctx, "sub_xxx", nil)if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.ReactivateSubscriptionParams;var commet = Commet.builder().apiKey("ck_xxx").build();var reactivatedSubscription = commet.subscriptions().reactivate(    "sub_xxx",    ReactivateSubscriptionParams.builder().build());
use Commet\Commet;$commet = new Commet('ck_xxx');$reactivatedSubscription = $commet->subscriptions->reactivate(id: 'sub_xxx');

If the customer must update their payment method, create a hosted recovery link:

import { Commet } from "@commet/node";const commet = new Commet({ apiKey: "ck_xxx" });const recoveryLink = await commet.subscriptions.createRecoveryLink({ id: "sub_xxx" });
from commet import Commetcommet = Commet("ck_xxx")recovery_link = commet.subscriptions.create_recovery_link("sub_xxx")
client, err := commet.New("ck_xxx")if err != nil {	log.Fatal(err)}ctx := context.Background()recoveryLink, err := client.Subscriptions.CreateRecoveryLink(ctx, "sub_xxx", nil)if err != nil {	log.Fatal(err)}
import co.commet.Commet;import co.commet.params.CreateSubscriptionRecoveryLinkParams;var commet = Commet.builder().apiKey("ck_xxx").build();var recoveryLink = commet.subscriptions().createRecoveryLink(    "sub_xxx",    CreateSubscriptionRecoveryLinkParams.builder().build());
use Commet\Commet;$commet = new Commet('ck_xxx');$recoveryLink = $commet->subscriptions->createRecoveryLink(id: 'sub_xxx');

Automatic dunning retries are anchored to the original decline on days 1, 3, 5, and 7. A successful retry returns the subscription to active.

Reactivate a canceled subscription

The same reactivate operation charges the saved payment method, reuses the subscription record, and starts a fresh period anchored to the reactivation date. You may pass an offerId; accepted phases are persisted as an immutable Offer Application.

The selected price is not snapshotted. Future renewals use its current catalog value. Archiving that price prevents new selection but does not break the existing subscription.

Related

  • Grant Temporary Plan Access
  • Upgrade and Downgrade Plans
  • Handle Failed Payments
  • Regional and Market Pricing
  • Introductory Offers
  • Customer Portal

How is this guide?

Trial Periods

Use free-trial Offer phases for automatic onboarding or explicit campaigns.

Grant Temporary Plan Access

Temporarily expand a subscription's features and limits without changing its plan, price, invoice, or billing cycle.

On this page

Lifecycle
Create
Retrieve current or historical state
Pause and resume
Choose when to pause
Set the duration
Create a pause
Edit or revoke a pause
Resume and handle payment outcomes
Keep access in sync
Cancel
Reverse a scheduled cancellation
Recover a past-due subscription
Reactivate a canceled subscription
Related