Manage Plan API

Programmatically manage a company via the Manage Plan API.

The Manage Plan API lets you programmatically set a company’s full plan state, including the base plan, add-ons, credit bundles, and pay-in-advance entitlements. It handles both Stripe-linked and non-billing plans in a single call.

This is a replace-style (PUT-like) API, not a patch. The request body represents the desired end state of the company’s plan configuration.

Anything you don’t include will be removed. That means:

  • An add-on omitted from add_on_selections will be removed from the company.
  • A pay-in-advance entitlement (e.g. user seats) omitted from pay_in_advance_entitlements will be removed, dropping the company’s quantity to zero.
  • A base plan omitted (or set to null) will be cleared, falling back to the default plan if one is configured.

When upgrading or making any change, you almost always want to read the company’s current state first and re-send the existing add-ons and pay-in-advance entitlements alongside whatever you’re changing. See Common patterns for examples.

Credit bundles are the exception. Unlike the other fields, credit_bundles is not part of the replace-style behavior. Any bundles you include are purchased immediately, added to the company’s credit ledger, and persist through future plan changes. Omitting them in a later request does not remove previously purchased credits. To send a request that doesn’t purchase any new credits, pass an empty array.

Endpoints

EndpointPurpose
POST /manage-planApply a plan change
POST /manage-plan/previewPreview the financial impact of a plan change without applying it. Useful for custom checkout scenarios.
POST /manage-plan/subscription/cancelCancel a company’s active subscription

All endpoints require API key authentication.

How it works

When you call POST /manage-plan, you provide the complete desired plan state for a company. Schematic then:

  1. Determines which plans are billing-linked (connected to Stripe) and which are not
  2. For billing-linked plans, updates the Stripe subscription accordingly
  3. For non-billing plans, applies the assignment directly
  4. Returns the updated company object

Because this is a replace-style API, you must always include the full desired state. The two fields most commonly missed:

  • add_on_selections — if a company currently has add-ons A and B, and you want to add C, you must include all three. Sending only C will remove A and B.
  • pay_in_advance_entitlements — if a company has 10 seats today and you make any plan change, you must include { price_id, quantity: 10 } in this array, or their seat count will be set to zero.

The safest pattern is: fetch the company’s current state, build the request from it, then mutate only what you intend to change.

Plan version behavior

When specifying a base plan or add-on:

  • If you provide an explicit version_id, that version is used
  • If the company is already on that plan and you omit the version, the company’s current version is preserved
  • If it’s a new plan and you omit the version, the latest published version is used

Default (fallback) plan

If you clear all plans from a company (by omitting base_plan_id), Schematic will assign the configured default/fallback plan if one exists.

Proration

When changing plans on a billing-linked subscription, Schematic uses Stripe’s proration logic to refund the unused portion of the current plan’s base cost and charge the prorated cost of the new plan. The same applies to add-on changes, credit bundles, and pay-in-advance quantity changes.

Upgrade vs. downgrade behavior differs:

  • Upgrades keep the current billing period (e.g. if you’re 15 days in, you stay on day 15 after the upgrade), and accrued usage on usage-based features carries over to the new plan
  • Downgrades reset the billing period (the new plan starts on day 1), which also resets usage counters for usage-based features

If your Schematic catalog has scheduled downgrades enabled, downgrades are deferred to the end of the current billing period instead of being applied immediately, and no proration is applied.

Use POST /manage-plan/preview to see the exact prorated charges and credits before applying a change.

Manage Plan

POST /manage-plan

Request body

1{
2 "company_id": "comp_abc123",
3 "base_plan_id": "plan_xyz789",
4 "base_plan_price_id": "bprice_def456",
5 "base_plan_version_id": "planv_ghi012",
6 "add_on_selections": [
7 {
8 "plan_id": "plan_addon1",
9 "price_id": "bprice_jkl345",
10 "version_id": "planv_mno678"
11 }
12 ],
13 "credit_bundles": [
14 {
15 "bundle_id": "bundle_pqr901",
16 "quantity": 5
17 }
18 ],
19 "pay_in_advance_entitlements": [
20 {
21 "price_id": "bprice_stu234",
22 "quantity": 10
23 }
24 ],
25 "coupon_external_id": "DISCOUNT20",
26 "promo_code": "LAUNCH2024",
27 "payment_method_external_id": "pm_stripe_abc",
28 "trial_end": "2024-12-31T23:59:59Z"
29}

Field reference

company_id
stringRequired

The ID of the company whose plan you want to manage.

base_plan_id
string

The plan to assign as the company’s base plan. Omit or set to null to clear the base plan (the company will fall back to the default plan if one is configured).

base_plan_price_id
string

The specific price to use for the base plan. Required for billing-linked plans with multiple price options (e.g. monthly vs. yearly).

base_plan_version_id
string

A specific version of the base plan. If omitted, the company’s current version is preserved (same plan) or the latest published version is used (new plan).

add_on_selections
array

The complete list of add-ons the company should have. Each entry requires a plan_id. Billing-linked add-ons also require a price_id. An optional version_id can pin a specific version.

This is replace-style. Any add-on the company currently has that is not included in this array will be removed. To preserve existing add-ons across a plan change, you must re-send them here.

credit_bundles
array

Credit bundles to purchase as part of this request. Each entry requires a bundle_id and quantity.

Unlike the other fields, this is not replace-style: bundles listed here are charged immediately and added to the company’s credit ledger. Previously purchased credits are not removed if you omit them. Pass an empty array to make a request that doesn’t purchase new credits.

pay_in_advance_entitlements
array

Pay-in-advance quantities (e.g. user seats). Each entry requires a price_id and quantity.

This is replace-style. Any pay-in-advance entitlement not included in this array will be set to zero. If a company currently has 10 seats and you want to keep them, you must re-send { price_id, quantity: 10 } on every request, even if you’re only changing something else (like the base plan or an add-on).

coupon_external_id
string

A Stripe coupon ID to apply to the subscription.

promo_code
string

A promotional code to apply.

payment_method_external_id
string

A Stripe payment method ID to use for this subscription.

trial_end
datetime

An ISO 8601 timestamp for when the trial should end. Must be in the future. To be eligible for a trial the plan must be marked as trialable, and cannot contain usage-based entitlements. Additionally, new add ons cannot be added when putting a company onto a trial.

Finding price IDs

The base_plan_price_id and add-on price_id fields are Schematic billing product price IDs (prefixed bprice_), not Stripe price IDs.

To find them, fetch a plan via GET /plans/:id (or list plans via GET /plans). The response includes a billing_product object with a prices array. Each entry has an id along with metadata like interval (month, year, one_time) and currency, so you can pick the right one.

1{
2 "id": "plan_xyz789",
3 "name": "Pro",
4 "billing_product": {
5 "prices": [
6 {
7 "id": "bprice_def456",
8 "interval": "month",
9 "currency": "usd",
10 "price": 4900
11 },
12 {
13 "id": "bprice_ghi789",
14 "interval": "year",
15 "currency": "usd",
16 "price": 49000
17 }
18 ]
19 }
20}

Response

1{
2 "data": {
3 "company": { ... },
4 "success": true
5 }
6}

The response includes the full updated company object reflecting the new plan state.

Preview Manage Plan

POST /manage-plan/preview

Use this endpoint to preview the financial impact of a plan change before applying it, most commonly while implementing your own custom checkout. The request body is identical to POST /manage-plan.

Response

1{
2 "data": {
3 "subscription_change_preview": {
4 ...
5 }
6 }
7}

The preview includes details like prorated charges, credits, and the new subscription amount. This is useful for showing users what a plan change will cost before they confirm.

Cancel Subscription

POST /manage-plan/subscription/cancel

Cancels a company’s active Stripe subscription. This only applies to companies with billing-linked plans.

Request body

1{
2 "company_id": "comp_abc123",
3 "cancel_immediately": true,
4 "prorate": true
5}
company_id
stringRequired

The ID of the company whose subscription you want to cancel.

cancel_immediately
boolean

If false, the subscription cancels at the end of the current billing period instead of immediately. Defaults to true.

prorate
boolean

If true and cancel_immediately is true, a prorated credit is issued for the unused portion of the billing period. Defaults to true.

Response

Same format as POST /manage-plan, returning the updated company object.

Common patterns

Upgrading a plan

To upgrade a company from one plan to another, send the new plan ID along with all existing add-ons and pay-in-advance entitlements you want to preserve. Schematic handles proration and Stripe subscription updates automatically.

In the example below, the company already has the team-collab add-on and 10 user seats. Both are re-sent on the request so they carry through the upgrade.

1{
2 "company_id": "comp_abc123",
3 "base_plan_id": "plan_pro",
4 "base_plan_price_id": "bprice_pro_monthly",
5 "add_on_selections": [
6 { "plan_id": "plan_team_collab", "price_id": "bprice_team_collab" }
7 ],
8 "pay_in_advance_entitlements": [
9 { "price_id": "bprice_seats", "quantity": 10 }
10 ]
11}

If you omit add_on_selections or pay_in_advance_entitlements (or send them empty), the company’s existing add-ons will be removed and their pay-in-advance quantities will drop to zero. Always re-send existing values when upgrading.

Adding an add-on while preserving existing state

To add an add-on without changing anything else, you must re-send the current base plan, all existing add-ons, and all existing pay-in-advance entitlements alongside the new add-on.

In the example below, the company is on the pro plan, has the team-collab add-on, and 10 user seats. We’re adding the priority-support add-on. The existing pro plan, team-collab add on, and 10 seats are re-sent so they aren’t removed.

1{
2 "company_id": "comp_abc123",
3 "base_plan_id": "plan_pro",
4 "base_plan_price_id": "bprice_pro_monthly",
5 "add_on_selections": [
6 { "plan_id": "plan_team_collab", "price_id": "bprice_team_collab" },
7 { "plan_id": "plan_priority_support", "price_id": "bprice_priority_support" }
8 ],
9 "pay_in_advance_entitlements": [
10 { "price_id": "bprice_seats", "quantity": 10 }
11 ]
12}

Forgetting any of base_plan_id, the existing add_on_selections, or pay_in_advance_entitlements here would silently change the company’s state. Omitting base_plan_id clears the plan (falling back to the default), omitting an existing add-on removes it, and omitting pay_in_advance_entitlements drops the seat count to zero. Always re-send everything you want to preserve.

Changing a pay-in-advance quantity (e.g. adding a seat)

To change just one pay-in-advance quantity, you still need to re-send the full plan state alongside it. Below, the company is going from 10 seats to 11.

1{
2 "company_id": "comp_abc123",
3 "base_plan_id": "plan_pro",
4 "base_plan_price_id": "bprice_pro_monthly",
5 "add_on_selections": [
6 { "plan_id": "plan_existing_addon", "price_id": "bprice_existing" }
7 ],
8 "pay_in_advance_entitlements": [
9 { "price_id": "bprice_seats", "quantity": 11 }
10 ]
11}

Error cases

The Manage Plan API validates the full request before applying any changes. If validation fails, the entire request is rejected and the company’s state is left untouched. Common error cases include:

  • Externally managed subscription: If the company’s subscription is managed outside of Schematic (e.g. directly in Stripe without the Schematic integration), the API will return an error. You must manage the subscription through whichever system owns it.
  • Invalid trial configuration: Trials require a trialable plan with no add-ons and no usage-based entitlements. If the plan is not marked as trialable, or you include add-ons or usage-based entitlements while setting trial_end, the request will be rejected. The trial_end timestamp must also be in the future.
  • Missing price for billing-linked add-on: Billing-linked add-ons require a price_id. Look up the available prices via GET /plans/:id.
  • Price specified for non-billing add-on: You cannot specify a price_id for an add-on that is not linked to a billing product. Omit the price_id for these add-ons.
  • Plan not found or not available to company: The base plan or an add-on does not exist, or the company is not eligible for it (e.g. it’s restricted by a plan audience).
  • Stripe errors: Any error returned by Stripe during the subscription update (e.g. payment method declined, customer not found, currency mismatch) will be surfaced back to you. The Schematic state is rolled back if the Stripe call fails.

For all errors, the response will include a descriptive message indicating what went wrong. We recommend using POST /manage-plan/preview before applying changes in production flows so users can be shown the financial impact and any validation issues up front.