---
title: "Quote or apply a plan upgrade"
method: POST
path: "/api/v1/subscription/upgrade"
tags: ["Subscription"]
---

# Quote or apply a plan upgrade

`POST /api/v1/subscription/upgrade`

## Request

- Base URL: `https://www.poster.ly`
- URL: `https://www.poster.ly/api/v1/subscription/upgrade`
- Auth: HTTP bearer

Move the workspace owner subscription to a strictly higher plan (`tier`). Never moves down; use the downgrade endpoint for that. Always quote first: send `dry_run: true` to get the exact prorated charge for today, the new recurring price and interval, and the new account limit and credits, without charging anything. Then send the same body without `dry_run` to apply it. Applying charges the saved card the prorated difference right away, at the same billing interval. With a working card the plan changes immediately (`status: applied`). If the card needs 3D Secure or is declined, NOTHING changes: the response is 200 with `status: payment_required` and a Stripe-hosted `hosted_invoice_url` for the owner to complete, and the plan changes automatically when that invoice is paid. Until then further changes are refused with 409 `payment_pending`. A trialing subscription keeps its trial: the price changes and nothing is charged today. The billing interval does not change in an upgrade. The one exception is Agency, which is monthly or yearly only: from a weekly plan, send `interval` as `month` or `year` (otherwise 409 `interval_unavailable`); any other interval change is 409 `interval_change_unsupported`. Refused with 409 and a `billing_url` to finish on the billing page: a subscription that includes the API and MCP add-on (`api_addon_requires_dashboard`); any discount other than a permanent percent-off below 100 (`discount_requires_dashboard`); a subscription schedule (`schedule_attached`); and an account without a card subscription (`no_subscription`). A standalone API and MCP add-on that would stop granting access on the target plan (Agency needs the Agency add-on) is also `api_addon_requires_dashboard`, but the response carries an `api_addon_url` pointing to the API page, where the add-on is switched. `request_id` (8 to 64 letters, digits, `_` or `-`) makes a retry safe: nothing is charged twice. A retry after the first attempt landed is refused (`not_an_upgrade` or `quote_stale`), not replayed. Leave it out to have one generated; a malformed one is 400 `invalid_request_id`. Use a fresh one for each attempt. `expected_current_tier` and `expected_interval` (optional) pin the quote you showed: if either changed since, nothing is charged and the answer is 409 `quote_stale`. Workspace owner only (403 `workspace_owner_required` for teammates, even with an owner-issued key). Requires the `billing:write` scope. Bypasses the API add-on paywall.

## Request body

- UpgradeSubscriptionRequest
  - `tier` 'starter' | 'pro' | 'power_user' | 'agency', required: The plan to move to. Must be strictly higher than the current plan (400 not_an_upgrade otherwise).
  - `interval` 'month' | 'year': Optional, and only for a plan whose current interval the target plan does not sell: Agency has no weekly price, so a weekly plan must send month or year (otherwise 409 interval_unavailable). The same interval as the current one is ignored; any other change of interval is 409 interval_change_unsupported (change the interval on the billing page).
  - `dry_run` boolean: true returns the exact quote and charges nothing.
  - `request_id` string: Makes a retry safe: nothing is charged twice. Leave it out to have one generated; a malformed one is 400 invalid_request_id. Use a fresh request_id for each attempt; the same id with a different request returns 409 request_id_reused. A retry after the first attempt landed is refused (not_an_upgrade or quote_stale), not replayed.
  - `expected_current_tier` 'starter' | 'pro' | 'power_user' | 'agency': Optional. The plan the quote was based on. If the live plan differs, nothing is charged and the answer is 409 quote_stale. The MCP confirm always sends it.
  - `expected_interval` 'week' | 'month' | 'year': Optional. The billing interval the quote was based on. If it changed, nothing is charged and the answer is 409 quote_stale.
  - `workspace_id` string: Optional workspace to act in.

## Response `200`

The quote (dry_run), the applied upgrade, or a payment link when the card needs confirmation.

- union
  - UpgradeSubscriptionQuoteResponse
    - `dry_run` true, required
    - `quote` object, required
      - `kind` 'upgrade', required
      - `current_tier` 'starter' | 'pro' | 'power_user' | 'agency'
      - `current_tier_name` string
      - `new_tier` 'starter' | 'pro' | 'power_user' | 'agency', required
      - `new_tier_name` string
      - `current_interval` 'week' | 'month' | 'year'
      - `interval` 'week' | 'month' | 'year', required: The interval after the upgrade.
      - `interval_changes` boolean
      - `trialing` boolean: On a trial the trial continues, nothing is charged today and the new price starts when it ends.
      - `charge_today_cents` integer, required: What the saved card is charged right now (prorated, after any discount). 0 on a trial.
      - `charge_today` string
      - `currency` string
      - `current_recurring_cents` integer, nullable: List price of the current plan per interval, before any discount.
      - `new_recurring_cents` integer, required: List price of the new plan per interval, before any discount.
      - `new_recurring` string
      - `account_limit_before` integer
      - `account_limit_after` integer
      - `included_credits_before` integer
      - `included_credits_after` integer
  - UpgradeSubscriptionAppliedResponse
    - `applied` true, required
    - `status` 'applied', required
    - `previous_tier` 'starter' | 'pro' | 'power_user' | 'agency'
    - `previous_tier_name` string
    - `tier` 'starter' | 'pro' | 'power_user' | 'agency', required
    - `tier_name` string
    - `interval` 'week' | 'month' | 'year'
    - `trialing` boolean
    - `charged_cents` integer, nullable: What the invoice for this change charged, when Stripe reports it. 0 on a trial.
    - `account_limit` integer
    - `included_credits` integer
  - UpgradeSubscriptionPaymentRequiredResponse
    - `applied` false, required
    - `status` 'payment_required', required
    - `payment_required` true, required
    - `message` string
    - `hosted_invoice_url` string, nullable, required: Send the owner here to complete the payment. The plan changes automatically once it is paid.
    - `payment_pending` UpgradePaymentPending
      - `hosted_invoice_url` string, nullable: Stripe-hosted payment page. The owner completes 3D Secure or retries the card there.
      - `requested_tier` 'starter' | 'pro' | 'power_user' | 'agency', nullable
      - `amount_due_cents` integer, nullable
      - `expires_at` string, date-time, nullable: After this time Stripe discards the pending change.
    - `previous_tier` 'starter' | 'pro' | 'power_user' | 'agency'
    - `previous_tier_name` string
    - `requested_tier` 'starter' | 'pro' | 'power_user' | 'agency'
    - `requested_tier_name` string
    - `interval` 'week' | 'month' | 'year'
    - `account_limit_after_payment` integer
    - `included_credits_after_payment` integer

## Other responses

- `400`: Invalid input (`invalid_tier`, `invalid_interval`, `invalid_dry_run`, `invalid_request_id`, `invalid_expected_current_tier`, `invalid_expected_interval`) or the target is not higher than the current plan (`not_an_upgrade`).
- `401`: Invalid or missing API key
- `402`: The card was declined at charge time, or a pending payment blocks another change (`payment_failed`) or the bank needs authentication that cannot be completed through a payment link (`payment_requires_authentication`). Nothing changed.
- `403`: Request is authenticated but not allowed
- `409`: The subscription cannot be upgraded from here: not active or trialing, paused, or cancelling (`subscription_not_eligible`, `cancel_at_period_end`), a subscription schedule is attached (`schedule_attached`), an earlier change is waiting on payment (`payment_pending`, with its payment link), an API and MCP add-on would be left behind or stop working (`api_addon_requires_dashboard`), a discount other than a permanent percent-off below 100 (`discount_requires_dashboard`), there is no card subscription (`no_subscription`), the target plan is not sold at this interval (`interval_unavailable`) or the interval cannot change in an upgrade (`interval_change_unsupported`), the quote is out of date (`quote_stale`), the request_id was used for a different request (`request_id_reused`), or the subscription changed concurrently (`concurrent_change`).
- `429`: Too many requests
- `502`: The change could not be priced right now (`quote_unavailable`). Try again.
- `503`: Billing is not configured, or the price for the target plan is not configured (`price_not_configured`). Nothing changed.

---

[API](https://skmtc.dev/poster/apis/posterly-api.md) · [All operations](https://skmtc.dev/poster/apis/posterly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/poster/apis/posterly-api/revisions/60be02a61f95?raw)
