> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suby.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Call off a scheduled plan change

> Drop a plan change that is queued and has **not taken effect yet** — the way back out for a customer who asked for a different plan and changed their mind before the next billing cycle.

This is what to call for a **downgrade scheduled at the period end**. The subscription is left exactly as it is: it stays on the plan it bills today, and the next renewal charges that plan's price. Nothing is refunded and nothing is charged, because the queued change had not billed anything yet.

Read what is queued from `GET /v3/subscriptions/{id}` → `scheduledChange` (`null` when nothing is queued). Pass `scheduledChangeId` to make the call idempotent against the change you actually read: if it no longer names the queued change, the request is refused instead of dropping a different one.

Changing the plan back by calling `change-plan` with the current `productId` does **not** work — that answers `409 SAME_PLAN`, because it would queue a second change rather than remove the first.

**An immediate upgrade cannot be called off here.** Its charge is already in flight (or has settled), so dropping the swap would leave the customer paying for a plan they never receive: answer is `409` `PLAN_CHANGE_NOT_CANCELABLE`. Refund the charge and call `change-plan` back to the previous plan instead. An upgrade whose charge was refused or abandoned never applies, and that one *can* be called off to clear the queue.




## OpenAPI

````yaml /v3-beta/api-reference/openapi.yaml post /v3/subscriptions/{id}/cancel-plan-change
openapi: 3.1.0
info:
  title: Suby.fi Merchant API
  version: 3.0.0-beta
  description: >
    The **v3** public merchant API for Suby.fi. RESTful (plural,
    resource-oriented

    endpoints under the `/v3` prefix). Authenticate every request

    with your secret API key in the `X-Suby-Api-Key` header.


    - `sk_live_…` keys operate in **production** (real funds).

    - `sk_sandbox_…` keys operate in a fully simulated **sandbox** (test cards,
      Base Sepolia crypto, no real money). The environment is derived from the
      key prefix.

    All monetary amounts are **integer cents** (except `priceCents` on some

    read shapes, which is a string) — token amounts are strings in the smallest

    unit (sats / lamports / wei). Fees are basis points.
  contact:
    email: dev@suby.fi
    url: https://suby.fi
servers:
  - url: https://api.beta.suby.fi
    description: Production + sandbox (environment selected by API key prefix)
security:
  - ApiKeyAuth: []
tags:
  - name: Products
    description: Manage your product catalog (one-time and recurring).
  - name: Checkout
    description: >-
      Hosted checkout — signed session tokens (`cs_…`) plus branding and
      appearance settings.
  - name: Payments
    description: >-
      Create, capture, void, and retrieve one-time & off-session payments (card,
      APM, and crypto).
  - name: Subscriptions
    description: Start and cancel recurring subscriptions.
  - name: Customers
    description: First-class customer records with billing address.
  - name: Payment Methods
    description: >-
      Save a payment method to a customer and manage saved instruments for
      off-session charges.
  - name: Webhook Endpoints
    description: Register outbound webhook destinations and rotate signing secrets.
  - name: Notifications
    description: Per-org email notification preferences — team alerts and customer emails.
  - name: Analytics
    description: Org-wide revenue analytics.
  - name: Health
    description: Unauthenticated liveness probe.
paths:
  /v3/subscriptions/{id}/cancel-plan-change:
    post:
      tags:
        - Subscriptions
      summary: Call off a scheduled plan change
      description: >
        Drop a plan change that is queued and has **not taken effect yet** — the
        way back out for a customer who asked for a different plan and changed
        their mind before the next billing cycle.


        This is what to call for a **downgrade scheduled at the period end**.
        The subscription is left exactly as it is: it stays on the plan it bills
        today, and the next renewal charges that plan's price. Nothing is
        refunded and nothing is charged, because the queued change had not
        billed anything yet.


        Read what is queued from `GET /v3/subscriptions/{id}` →
        `scheduledChange` (`null` when nothing is queued). Pass
        `scheduledChangeId` to make the call idempotent against the change you
        actually read: if it no longer names the queued change, the request is
        refused instead of dropping a different one.


        Changing the plan back by calling `change-plan` with the current
        `productId` does **not** work — that answers `409 SAME_PLAN`, because it
        would queue a second change rather than remove the first.


        **An immediate upgrade cannot be called off here.** Its charge is
        already in flight (or has settled), so dropping the swap would leave the
        customer paying for a plan they never receive: answer is `409`
        `PLAN_CHANGE_NOT_CANCELABLE`. Refund the charge and call `change-plan`
        back to the previous plan instead. An upgrade whose charge was refused
        or abandoned never applies, and that one *can* be called off to clear
        the queue.
      operationId: cancelScheduledPlanChange
      parameters:
        - $ref: '#/components/parameters/SubscriptionId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                scheduledChangeId:
                  type: string
                  description: >-
                    Optional guard — the change you read from `GET
                    /v3/subscriptions/{id}`. When it does not name the queued
                    change the request is refused (`NO_SCHEDULED_PLAN_CHANGE`)
                    rather than applied to whatever is queued.
                  example: ssc_abc123
      responses:
        '200':
          description: The queued change was called off
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - properties:
                      data:
                        type: object
                        properties:
                          subscription:
                            $ref: '#/components/schemas/Subscription'
                          scheduledChange:
                            allOf:
                              - $ref: '#/components/schemas/ScheduledChange'
                            description: >-
                              The called-off change, now carrying `canceledAt`.
                              It will never apply.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >-
            Subscription not found, or nothing queued to call off
            (`SUBSCRIPTION_NOT_FOUND`, `NO_SCHEDULED_PLAN_CHANGE`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            The change can no longer be called off — an immediate upgrade whose
            charge is in flight, or one that took effect first
            (`PLAN_CHANGE_NOT_CANCELABLE`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    SubscriptionId:
      name: id
      in: path
      required: true
      schema:
        type: string
        example: sub_abc123
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Optional key (≤255 chars, e.g. a UUID v4) that makes this POST safe to
        retry: the first request executes and its response is cached for 24h; a
        retry with the SAME key replays that response instead of re-executing
        (no duplicate payment/subscription). A reused key with a different
        request → `422 IDEMPOTENCY_KEY_CONFLICT`; a retry while the first is
        still in flight → `409`. See the Idempotency guide.
      schema:
        type: string
        maxLength: 255
        example: 5f3b9c2e-1a4d-4f2b-9c31-7e2a1b6d8c04
  schemas:
    SuccessEnvelope:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
        data:
          description: Endpoint-specific payload.
    Subscription:
      type: object
      properties:
        id:
          type: string
          example: sub_abc123
        organizationId:
          type: string
        customerId:
          type: string
        productId:
          type: string
        status:
          type: string
          enum:
            - INCOMPLETE
            - TRIALING
            - PENDING_3DS
            - PENDING_REDIRECT
            - ACTIVE
            - PAST_DUE
            - PAUSED
            - CANCELED
            - EXPIRED
        currentCycle:
          type: integer
        totalCycles:
          type: integer
          nullable: true
        trialEndAt:
          type: string
          format: date-time
          nullable: true
        cancelAtPeriodEnd:
          type: boolean
        currentCycleDueAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            Period-end / access-expiry clock for BOTH rails (replaces the
            removed `expiresAt`): card subs renew off-session at this date,
            crypto subs renew by email around it. Null once no further renewal
            is scheduled (final cycle reached or terminated).
        renewalAttempt:
          type: integer
          description: >-
            Attempts made for the current cycle: 0 while healthy, 1..8 while
            dunning (card smart-retry).
        nextRenewalAttemptAt:
          type: string
          format: date-time
          nullable: true
          description: Scheduled next retry while PAST_DUE (card back-off).
        lastDeclineCategory:
          type: string
          nullable: true
          enum:
            - SOFT
            - HARD
          description: >-
            Category of the most recent decline: SOFT (retried) or HARD (ends
            the subscription).
        externalRef:
          type: string
          nullable: true
          description: Merchant business reference set at create.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ScheduledChange:
      type: object
      description: >-
        An upgrade/downgrade recorded by `POST /change-plan` — pending while
        `appliedAt` and `canceledAt` are both null.
      properties:
        id:
          type: string
          example: ssc_abc123
        type:
          type: string
          enum:
            - UPGRADE
            - DOWNGRADE
            - INSTRUMENT_CHANGE
        targetPriceId:
          type: string
          nullable: true
          description: Target plan (recurring Product) id the subscription switches to.
          example: pro_9m2k1x8s7d6f
        scheduledFor:
          type: string
          format: date-time
          description: >-
            When the change applies: `now` for an immediate upgrade, the period
            end for a downgrade.
        appliedAt:
          type: string
          format: date-time
          nullable: true
          description: Null while pending; set once the plan swap has been applied.
        canceledAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            Set when the change was called off before it took effect (`POST
            /cancel-plan-change`, or superseded by a newer scheduled change). It
            will never apply. A change is PENDING while `appliedAt` and
            `canceledAt` are both null.
    Error:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Machine-readable error code.
          example: NOT_FOUND
        message:
          type: string
          example: Resource not found
        data:
          description: Optional error detail.
  responses:
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error: UNAUTHORIZED
            message: Invalid or missing API key
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Suby-Api-Key
      description: Secret API key. `sk_live_…` (production) or `sk_sandbox_…` (sandbox).

````