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

# Cancel a subscription

> Cancels a subscription and returns the attempt this request recorded. `mode` decides when it
ends: `at_period_end` lets the period the buyer has already paid for run out, `immediate` ends
it now. A subscription in `trial` can only be cancelled `immediate`.

**Nothing is refunded or prorated, in either mode.** To return money to the buyer, refund the
payment through `POST /v1/commerce/payments/{id}/refunds`.

**A 200 means the cancellation was accepted, not that the subscription has ended.** It moves to
`canceling` — never straight to `cancelled` — and `cancel_at` carries when it ends: the end of
the paid period for `at_period_end`, the moment of the request for `immediate`. It reads
`cancelled` once the payment provider has actually ended it.

**`subscription.cancelled` is not sent when you call this.** That event is written when the
provider ends the subscription — for `at_period_end`, at the end of the paid period rather than
now — and it carries `cancellation_reason` `business_cancel`.

Refused with 422 `subscription_not_eligible` when the subscription is already ending
(`already_canceling`), is not one a cancel can end (`not_active`: only `active`, `overdue`,
`paused` and `trial` can be), is in `trial` and was sent `at_period_end`
(`trial_requires_immediate`), has no subscription at the payment provider
(`missing_processor_subscription`), or has another change still processing
(`change_in_flight`). An `at_period_end` cancel of an `active` or `overdue` subscription is
also refused inside the 60 minutes before its renewal
(`too_close_to_renewal`, a 422 of its own) or when no renewal date is known
(`renewal_date_unknown`) — the renewal it would stop may already be billing. Neither can reach
an `immediate` cancel or a `paused` subscription.

The body is optional and carries one member: `reason`, your own note, recorded on the attempt and returned with it. Text longer than 500 characters is truncated. `mode` is required.



## OpenAPI

````yaml /api-reference/commerce-v1.openapi.json post /v1/commerce/subscriptions/{id}/cancel
openapi: 3.0.3
info:
  description: >-
    Public REST API for COPE vendor integrations. Authenticate with a COPE API
    key or Clerk bearer token and call the endpoints described below.
  title: COPE Public API
  version: v1
servers:
  - description: Production
    url: https://api.cope.com
security:
  - cope_sk: []
paths:
  /v1/commerce/subscriptions/{id}/cancel:
    post:
      tags:
        - Subscriptions
      summary: Cancel a subscription
      description: >-
        Cancels a subscription and returns the attempt this request recorded.
        `mode` decides when it

        ends: `at_period_end` lets the period the buyer has already paid for run
        out, `immediate` ends

        it now. A subscription in `trial` can only be cancelled `immediate`.


        **Nothing is refunded or prorated, in either mode.** To return money to
        the buyer, refund the

        payment through `POST /v1/commerce/payments/{id}/refunds`.


        **A 200 means the cancellation was accepted, not that the subscription
        has ended.** It moves to

        `canceling` — never straight to `cancelled` — and `cancel_at` carries
        when it ends: the end of

        the paid period for `at_period_end`, the moment of the request for
        `immediate`. It reads

        `cancelled` once the payment provider has actually ended it.


        **`subscription.cancelled` is not sent when you call this.** That event
        is written when the

        provider ends the subscription — for `at_period_end`, at the end of the
        paid period rather than

        now — and it carries `cancellation_reason` `business_cancel`.


        Refused with 422 `subscription_not_eligible` when the subscription is
        already ending

        (`already_canceling`), is not one a cancel can end (`not_active`: only
        `active`, `overdue`,

        `paused` and `trial` can be), is in `trial` and was sent `at_period_end`

        (`trial_requires_immediate`), has no subscription at the payment
        provider

        (`missing_processor_subscription`), or has another change still
        processing

        (`change_in_flight`). An `at_period_end` cancel of an `active` or
        `overdue` subscription is

        also refused inside the 60 minutes before its renewal

        (`too_close_to_renewal`, a 422 of its own) or when no renewal date is
        known

        (`renewal_date_unknown`) — the renewal it would stop may already be
        billing. Neither can reach

        an `immediate` cancel or a `paused` subscription.


        The body is optional and carries one member: `reason`, your own note,
        recorded on the attempt and returned with it. Text longer than 500
        characters is truncated. `mode` is required.
      operationId: commerce.subscriptions.cancel
      parameters:
        - description: id public identifier.
          example: sub_A1b2C3d4E5f6G7h8
          in: path
          name: id
          required: true
          schema:
            example: sub_A1b2C3d4E5f6G7h8
            pattern: ^sub_[A-Za-z0-9]{8,32}$
            type: string
        - description: >-
            Required. At most 255 characters of valid UTF-8 with no NUL byte.
            The change attempt is recorded against this key for this
            subscription: a retry that carries the same key and the same body
            returns the original response instead of changing the subscription
            twice, and the same key with a different body is refused with 409
            idempotency_conflict.
          in: header
          name: Idempotency-Key
          required: true
          schema:
            maxLength: 255
            type: string
      requestBody:
        content:
          application/json:
            examples:
              at_period_end:
                summary: End it when the period the buyer has already paid for runs out
                value:
                  mode: at_period_end
                  reason: Buyer asked to stop at the end of the month
              immediate:
                summary: End it now — the only mode a subscription in trial accepts
                value:
                  mode: immediate
            schema:
              additionalProperties: false
              properties:
                mode:
                  description: >-
                    `immediate` ends the subscription now; `at_period_end` lets
                    it run to the end of the period already paid for and ends it
                    there. Neither refunds anything.
                  enum:
                    - immediate
                    - at_period_end
                  type: string
                reason:
                  description: >-
                    Optional. Recorded on the attempt and returned as `reason`;
                    longer text is truncated to 500 characters.
                  maxLength: 500
                  type: string
              required:
                - mode
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                additionalProperties: false
                properties:
                  data:
                    additionalProperties: false
                    properties:
                      failure:
                        additionalProperties: false
                        nullable: true
                        properties:
                          code:
                            type: string
                          message:
                            type: string
                        required:
                          - code
                          - message
                        type: object
                      mode:
                        enum:
                          - immediate
                          - at_period_end
                          - null
                        nullable: true
                        type: string
                      reason:
                        nullable: true
                        type: string
                      requested_by:
                        additionalProperties: false
                        properties:
                          actor_id:
                            type: string
                          actor_type:
                            type: string
                          business_id:
                            type: string
                          initiated_by:
                            type: string
                        required:
                          - initiated_by
                          - actor_type
                          - actor_id
                          - business_id
                        type: object
                      status:
                        description: >-
                          What became of the attempt. `succeeded`: it was
                          applied. `failed`: it was refused, and `failure`
                          carries the code and the message saying why — nothing
                          changed. `processing`: it is still in flight.
                        enum:
                          - processing
                          - succeeded
                          - failed
                        type: string
                      subscription_id:
                        type: string
                      type:
                        enum:
                          - pause
                          - resume
                          - cancel
                        type: string
                    required:
                      - subscription_id
                      - type
                      - mode
                      - status
                      - reason
                      - requested_by
                      - failure
                    type: object
                required:
                  - data
                type: object
          description: Successful response
        '400':
          content:
            application/problem+json:
              examples:
                subscription_idempotency_key_required:
                  summary: Missing Idempotency-Key header
                  value:
                    code: idempotency_key_required
                    detail: >-
                      an Idempotency-Key header is required for a subscription
                      change
                    errors:
                      - code: idempotency_key_required
                        detail: >-
                          an Idempotency-Key header is required for a
                          subscription change
                        param: idempotency_key
                    request_id: req_123
                    status: 400
                    title: Idempotency Key Required
                    type: https://docs.cope.com/errors/idempotency_key_required
                subscription_invalid_idempotency_key:
                  summary: Malformed Idempotency-Key header
                  value:
                    code: invalid_idempotency_key
                    detail: >-
                      Idempotency-Key header must be at most 255 characters of
                      valid UTF-8 without a NUL byte
                    errors:
                      - code: invalid_idempotency_key
                        detail: >-
                          Idempotency-Key header must be at most 255 characters
                          of valid UTF-8 without a NUL byte
                        param: idempotency_key
                    request_id: req_123
                    status: 400
                    title: Invalid Idempotency Key
                    type: https://docs.cope.com/errors/invalid_idempotency_key
                unparsable_request:
                  summary: Unparsable request
                  value:
                    code: invalid_request
                    detail: >-
                      The request could not be parsed. Check the query string
                      and the request body.
                    request_id: req_123
                    status: 400
                    title: Invalid Request
                    type: https://docs.cope.com/errors/invalid_request
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Invalid request
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Missing or invalid bearer token
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Bearer token is not authorized for this route
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Resource not found
        '409':
          content:
            application/problem+json:
              examples:
                subscription_idempotency_conflict:
                  summary: Idempotency key reused with a different body
                  value:
                    code: idempotency_conflict
                    detail: >-
                      request body does not match the original request for this
                      idempotency key
                    errors:
                      - code: idempotency_body_mismatch
                        detail: >-
                          request body does not match the original request for
                          this idempotency key
                        param: idempotency_key
                    request_id: req_123
                    status: 409
                    title: Idempotency Conflict
                    type: https://docs.cope.com/errors/idempotency_conflict
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: >-
            The idempotency key was already used for this subscription with a
            different body.
        '422':
          content:
            application/problem+json:
              examples:
                subscription_status_validation_failed:
                  summary: The body carries an unsupported member
                  value:
                    code: validation_failed
                    detail: null
                    errors:
                      - code: unsupported
                        detail: resumes_at is not supported by this public endpoint
                        param: resumes_at
                    request_id: req_123
                    status: 422
                    title: Validation Failed
                    type: https://docs.cope.com/errors/validation_failed
                too_close_to_renewal:
                  summary: The next renewal is inside the change cutoff
                  value:
                    code: too_close_to_renewal
                    detail: too close to renewal
                    errors:
                      - code: too_close_to_renewal
                        detail: too close to renewal
                        param: subscription_id
                    request_id: req_123
                    status: 422
                    title: Too Close To Renewal
                    type: https://docs.cope.com/errors/too_close_to_renewal
                subscription_not_eligible:
                  summary: The subscription cannot be changed
                  value:
                    code: subscription_not_eligible
                    detail: installment plan
                    errors:
                      - code: installment_plan
                        detail: installment plan
                        param: subscription_id
                    request_id: req_123
                    status: 422
                    title: Subscription Not Eligible
                    type: https://docs.cope.com/errors/subscription_not_eligible
                subscription_trial_requires_immediate:
                  summary: A subscription in trial was sent at_period_end
                  value:
                    code: subscription_not_eligible
                    detail: trial requires immediate
                    errors:
                      - code: trial_requires_immediate
                        detail: trial requires immediate
                        param: subscription_id
                    request_id: req_123
                    status: 422
                    title: Subscription Not Eligible
                    type: https://docs.cope.com/errors/subscription_not_eligible
                subscription_already_canceling:
                  summary: The subscription is already ending
                  value:
                    code: subscription_not_eligible
                    detail: already canceling
                    errors:
                      - code: already_canceling
                        detail: already canceling
                        param: subscription_id
                    request_id: req_123
                    status: 422
                    title: Subscription Not Eligible
                    type: https://docs.cope.com/errors/subscription_not_eligible
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: The body is invalid, or the subscription cannot be changed now
      security:
        - cope_sk: []
components:
  schemas:
    PublicProblemDetail:
      additionalProperties: false
      properties:
        code:
          type: string
        detail:
          nullable: true
          type: string
        errors:
          items:
            additionalProperties: false
            properties:
              code:
                type: string
              detail:
                type: string
              param:
                type: string
            required:
              - code
              - detail
            type: object
          type: array
        request_id:
          type: string
        status:
          type: integer
        title:
          type: string
        type:
          type: string
      required:
        - type
        - title
        - status
        - code
        - request_id
      type: object
  securitySchemes:
    cope_sk:
      description: >-
        Bearer credential for the public API. Vendor integrations should send a
        live COPE API key (`ck_live_*`; keys issued earlier as `cope_sk_live_*`
        keep working). Clerk bearer tokens are also accepted when paired with
        `X-Cope-Business-Id`.
      scheme: bearer
      type: http

````