---
title: "Subscriptions"
description: "Read and manage Reload recurring orders — reschedule, pause, resume, skip and cancel"
---

> API Reference Index
> Fetch the complete API index at: https://docs.firearmcart.com/api/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscriptions

> **Requires the Reload plugin.** Subscriptions are a Reload feature. If your store does not have Reload installed and active, there is nothing for these endpoints to act on — and, importantly, **deactivating Reload stops all recurring charges**, including for subscriptions that already exist. See [What happens without Reload](#what-happens-without-reload) for the exact behaviour.

A subscription is a recurring order created by the Reload plugin: a set of line items, a cadence, a stored card and a shipping address that together produce a new order every cycle. These endpoints let you act on those subscriptions from your own systems the same way a merchant would from the dashboard — every change routes through the same service the customer self-serve portal uses and lands in the subscription's audit trail with the actor recorded as `merchant`.

Use them to build a subscriber view in your CRM, to honour a "pause my box" request from your own support desk, or to reconcile upcoming charges against inventory. Read access needs the `subscriptions:read` ability; every mutation needs `subscriptions:write`. See [Authentication](/api/authentication) for how abilities are attached to a token. All seven endpoints share the standard 60 requests-per-minute-per-team budget.

## What this API cannot do

There is **no create and no delete.** Subscriptions come into existence only when a shopper checks out through a Reload-enabled storefront, and they are ended by cancelling, never removed. Cancellation is also one-way here: `/resume` explicitly refuses a `cancelled` subscription, and the reactivation path exists only in the dashboard.

## What happens without Reload

The routes themselves are **not** gated — they return `200`, not a `403`, whether or not the plugin is active. What changes is what exists and what runs:

| Reload state | Reading | Recurring charges |
|---|---|---|
| Installed and active | Normal | Run hourly as scheduled |
| Never installed | Valid, empty `data` array | Nothing to charge |
| Installed then **deactivated** | Existing subscriptions still list and still accept mutations | **Stop immediately** |

That third row is the one that catches people. Deactivating Reload halts billing for every subscription on the store — the hourly charge run filters to teams with the plugin active, so a subscription can sit in this API reporting `status: "active"` with a `next_charge_at` in the past while nothing is actually being billed. Charges resume on the next hourly tick if the merchant reactivates.

Because the routes never error, an empty `data` array is not evidence of a misconfiguration, and a populated one is not evidence that billing is running. If you are reconciling revenue, confirm the plugin state in the dashboard rather than inferring it from this API.

## Subscriptions are keyed by UUID

Every other v1 resource is addressed by its per-store number. Subscriptions are the exception: the path parameter is the `uuid` field, and `subscription_id` is a display number for merchant-facing screens only. Passing a `subscription_id` — or anything else that is not a well-formed UUID — does not return a 404: the path segment reaches a UUID-typed column unguarded and the request fails as a 500. A `404` means a well-formed UUID that no subscription on your store carries.

The `customer_id` on a subscription is the same value the Customers API returns, so the two line up. The `shipping_address_id` and `payment_method_id` accepted by `PATCH` are UUIDs: take `address_id` from any address object in a v1 response, and `payment_method_id` from the subscription's own `payment_method` block.

## Every mutation returns 200, even when nothing happened

This is the single most important thing to know about the four action endpoints. Pause, resume, skip and cancel each check an eligibility rule before doing any work, and when that rule fails they **return early and still respond `200` with the unchanged subscription.** There is no error, no flag, and no distinguishing field.

| Action | Only takes effect when status is |
| --- | --- |
| `pause` | `active` |
| `resume` | `paused` or `past_due` |
| `skip` | `active` |
| `cancel` | `active`, `paused` or `past_due` |

`PATCH` behaves the same way for cadence: sending the cadence the subscription already has writes nothing and logs no event.

Always read the returned resource rather than trusting the status code. Check `data.status` after pause, resume and cancel, and compare `data.next_charge_at` against the value you started from after a skip.

## Reading the response

A few shapes are easy to get wrong:

- **Money mixes units.** `items[].unit_price` is in **dollars** (`24.99`), not cents. `discount_pct` is a whole percentage between 0 and 50 applied to the recurring subtotal — `10` means 10% off, not $10 and not 0.1. Whole values lose their decimal point in JSON, so both fields arrive as integers on some rows and floats on others; parse them as numbers.
- **`shipping_address` is never `null`.** When there is no address, or the customer soft-deleted the one the subscription pointed at, the key is still an object: `address_id` and every street field read `null`, the two role booleans read `false`, and `type` still reads `"shipping"`. Test `shipping_address.address_line1`, not the key. `payment_method` and `customer_id`, by contrast, *do* come back as `null` in the equivalent situation.
- **Some stored state is not exposed.** `paused_until`, `cancelled_at`, `cancellation_reason`, the last decline type, the billing address and the merchant account have no representation in the response, so a reason you write via `cancel` cannot be read back.
- **Pagination links drop your filters.** `links.next` and the entries in `meta.links` carry only `?page=N`. Re-apply `status`, `customer_id` and `per_page` on each request instead of following those URLs as-is.

## Timestamps

All timestamps are ISO 8601 with an explicit UTC offset (`2026-09-01T14:00:00+00:00`). Cadence arithmetic is calendar-based — one, three, six or twelve months — not a fixed number of days, and it preserves the time of day, so a subscription that bills at 14:00 on the 1st keeps billing at 14:00 on the 1st.

## List subscriptions

`GET /v1/subscriptions`

Returns the token team's Reload subscriptions, newest first, in a standard length-aware page. Every relation the resource can show — items, shipping address, payment method and customer — is eager-loaded, so list rows carry the same fields as a single fetch.

- **Operation id:** `subscriptions.list`
- **Required ability:** `subscriptions:read`
- **Rate limit:** 60 requests/minute per team

#### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | no | Exact-match filter on subscription status. One of `active`, `paused`, `past_due`, `cancelled`. Example: `active` |
| `customer_id` | integer | no | Restrict to one customer, matched on the `customer_id` the Customers API returns — the store-scoped number, not a database key. Example: `1187` |
| `per_page` | integer | no | Results per page. Defaults to 25, capped at 100. Example: `50` |
| `page` | integer | no | 1-based page number. Defaults to 1. Example: `2` |

#### Request

```bash
curl "https://api.firearmcart.com/v1/subscriptions?status=active&per_page=25" \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

#### Responses

##### 200 — A page of subscriptions. The second row shows what an absent shipping address and a removed card look like.

```json
{
  "data": [
    {
      "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
      "subscription_id": 1042,
      "status": "active",
      "cadence": "monthly",
      "cadence_label": "Monthly",
      "discount_pct": 10,
      "next_charge_at": "2026-09-01T14:00:00+00:00",
      "last_charged_at": "2026-08-01T14:00:00+00:00",
      "failed_charge_count": 0,
      "customer_id": 1187,
      "items": [
        {
          "product_id": 2043,
          "variant_id": 5581,
          "quantity": 2,
          "unit_price": 24.99
        }
      ],
      "shipping_address": {
        "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
        "type": "shipping",
        "address_line1": "418 Ridgeline Dr",
        "address_line2": null,
        "city": "Bozeman",
        "state": "MT",
        "zip_code": "59718",
        "is_billing": false,
        "is_shipping": true,
        "is_default_billing": false,
        "is_default_shipping": true
      },
      "payment_method": {
        "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
        "brand": "visa",
        "last_four": "4242"
      },
      "created_at": "2026-03-14T09:22:07+00:00",
      "updated_at": "2026-08-01T14:00:03+00:00"
    },
    {
      "uuid": "3d6b90a7-52c1-4f0e-8a44-b17e9c25d380",
      "subscription_id": 1039,
      "status": "paused",
      "cadence": "quarterly",
      "cadence_label": "Every 3 months",
      "discount_pct": 0,
      "next_charge_at": "2026-10-02T16:30:00+00:00",
      "last_charged_at": "2026-07-02T16:30:00+00:00",
      "failed_charge_count": 0,
      "customer_id": 1204,
      "items": [
        {
          "product_id": 1877,
          "variant_id": null,
          "quantity": 1,
          "unit_price": 189
        }
      ],
      "shipping_address": {
        "address_id": null,
        "type": "shipping",
        "address_line1": null,
        "address_line2": null,
        "city": null,
        "state": null,
        "zip_code": null,
        "is_billing": false,
        "is_shipping": false,
        "is_default_billing": null,
        "is_default_shipping": null
      },
      "payment_method": null,
      "created_at": "2026-01-08T18:41:55+00:00",
      "updated_at": "2026-07-19T11:02:14+00:00"
    }
  ],
  "links": {
    "first": "https://api.firearmcart.com/v1/subscriptions?page=1",
    "last": "https://api.firearmcart.com/v1/subscriptions?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "page": null,
        "active": false
      },
      {
        "url": "https://api.firearmcart.com/v1/subscriptions?page=1",
        "label": "1",
        "page": 1,
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "page": null,
        "active": false
      }
    ],
    "path": "https://api.firearmcart.com/v1/subscriptions",
    "per_page": 25,
    "to": 2,
    "total": 2
  }
}
```

##### 403 — The token is valid but was not granted the `subscriptions:read` ability.

```json
{
  "message": "Invalid ability provided."
}
```

#### Caveats

- Pagination links drop your filters. `paginate()` is called without `withQueryString()`, so `links.next` and `meta.links[].url` contain only `?page=N` — following them verbatim silently widens the result set. Re-apply `status`, `customer_id` and `per_page` yourself on every page request.
- `status` is not validated. An unrecognised value is passed straight into the `where` clause and returns an empty `data` array with `total: 0`, not a 422.
- `customer_id` is not validated either, and it maps to a bigint column — a non-numeric value (`?customer_id=abc`) reaches the database as an invalid integer literal and surfaces as a 500.
- `per_page` is capped but not floored: `per_page=0` falls back to an internal default of 15, and a negative value drops the SQL `LIMIT` entirely and returns every subscription the team has in a single page.
- Money units differ by field. `items[].unit_price` is **dollars** (the stored cents divided by 100) while `discount_pct` is a whole percentage in the range 0–50 — `10` means 10% off the recurring subtotal, not $10 and not 0.1.
- PHP serializes whole-dollar and whole-percent values without a decimal point, so `unit_price` and `discount_pct` arrive as JSON integers (`189`, `10`) on some rows and JSON floats (`24.99`) on others. Parse them as numbers, not as fixed-shape decimals.

## Retrieve a subscription

`GET /v1/subscriptions/{uuid}`

Fetches one subscription by its UUID, scoped to the token's team. The response is the same object the list endpoint returns, wrapped in `data`.

- **Operation id:** `subscriptions.get`
- **Required ability:** `subscriptions:read`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Subscriptions are the one v1 resource keyed by UUID rather than a per-store number — the `subscription_id` in the response is a display number and is not accepted here. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Request

```bash
curl https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31 \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

#### Responses

##### 200 — The subscription.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "active",
    "cadence": "monthly",
    "cadence_label": "Monthly",
    "discount_pct": 10,
    "next_charge_at": "2026-09-01T14:00:00+00:00",
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-01T14:00:03+00:00"
  }
}
```

##### 404 — No subscription with that well-formed UUID belongs to the token's team — a cross-team UUID lands here too, so existence is never leaked. A malformed UUID does NOT: see the notes.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- A malformed `uuid` is a 500, not a 404. None of the seven subscription routes check the path segment's format before querying, so a value that is not a well-formed UUID — a `subscription_id`, a truncated string — reaches the UUID-typed column and the database rejects it. Only a well-formed UUID that does not exist on your store returns the 404 above.
- Several stored fields are deliberately not exposed: `paused_until`, `cancelled_at`, `cancellation_reason`, `last_decline_type`, `billing_address_id` and `merchant_account_id` have no representation in the response.
- `shipping_address` is never `null`. When the subscription has no shipping address — or the customer soft-deleted the one it pointed at — the key is still an object, with every field `null`, `is_billing`/`is_shipping` `false` and `type` `"shipping"`. Test `shipping_address.address_line1` rather than the key itself.
- `payment_method` and `customer_id` *do* go `null` when their records are missing or soft-deleted, so the two nested objects behave inconsistently with each other.
- `payment_method` carries the card's `payment_method_id` UUID plus `brand` and `last_four` — the id is what `PATCH` accepts back.

## Update a subscription

`PATCH /v1/subscriptions/{uuid}`

Changes billing cadence, shipping address and/or stored card. Any subset of the three may be sent; each is applied through the same Reload service the customer portal uses, so each writes its own audit event. Returns the refreshed subscription.

- **Operation id:** `subscriptions.update`
- **Required ability:** `subscriptions:write`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `cadence` | string | no | New billing interval. One of `monthly`, `quarterly`, `semiannual`, `yearly`. Anything else is a validation error. Example: `quarterly` |
| `shipping_address_id` | string (uuid) | no | The `address_id` UUID of an address belonging to this subscription's customer — the value every address object in v1 responses carries (customer show, order show, this resource's `shipping_address`). Example: `6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3` |
| `payment_method_id` | string (uuid) | no | The `payment_method_id` UUID of a stored card belonging to this subscription's customer — returned inside this resource's own `payment_method` object. Example: `2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4` |

#### Request

```bash
curl -X PATCH https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31 \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"cadence": "quarterly"}'
```

#### Responses

##### 200 — The updated subscription. Note `next_charge_at` has been recomputed from the moment of the request, not carried over.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "active",
    "cadence": "quarterly",
    "cadence_label": "Every 3 months",
    "discount_pct": 10,
    "next_charge_at": "2026-11-11T09:15:44+00:00",
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-11T09:15:44+00:00"
  }
}
```

##### 422 — `shipping_address_id` did not resolve to a live address on this subscription's customer. `payment_method_id` fails the same way with `error: "payment_method_not_found"`.

```json
{
  "success": false,
  "error": "address_not_found",
  "message": "Address not found for this subscription customer."
}
```

##### 404 — No such subscription for the token's team.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- Two different 422 envelopes come out of this one endpoint. A bad `cadence` returns the stock validation shape — `message` plus an `errors` map keyed by field — while a bad `shipping_address_id` or `payment_method_id` returns the bespoke `success`/`error`/`message` shape shown above. Branch on the presence of `errors`, not on the status code.
- The stock validation envelope only appears if you send `Accept: application/json`. The application registers no custom exception rendering, so without that header the failure is treated as a browser form post and answered with a redirect.
- Changing cadence resets the billing anniversary. `next_charge_at` is recomputed as *now* plus one new interval, discarding whatever date was scheduled. Sending the cadence the subscription already has is a no-op — no date change and no audit event.
- Updates are applied in order — cadence, then address, then card — and are not wrapped in a transaction. A request carrying a valid `cadence` and an invalid `shipping_address_id` returns 422 with the cadence change **already committed**.
- Both ids are UUIDs scoped to `subscription.customer_id`. Discover them in v1: every address object carries `address_id`, and this resource's `payment_method` carries `payment_method_id`. A value that belongs to a different customer is a 422, not a cross-customer update.
- Soft-deleted addresses and cards are excluded by their models' global scopes, so an id the customer has since removed returns the not-found 422 rather than silently rebilling to it.

## Pause a subscription

`POST /v1/subscriptions/{uuid}/pause`

Suspends future charges indefinitely and moves the subscription to `paused`. Takes no body. Returns the refreshed subscription.

- **Operation id:** `subscriptions.pause`
- **Required ability:** `subscriptions:write`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Request

```bash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/pause \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

#### Responses

##### 200 — Paused. `next_charge_at` is retained but will not fire.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "paused",
    "cadence": "monthly",
    "cadence_label": "Monthly",
    "discount_pct": 10,
    "next_charge_at": "2026-09-01T14:00:00+00:00",
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-11T09:15:44+00:00"
  }
}
```

##### 404 — No such subscription for the token's team.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- **Silent no-op outside `active`.** `ReloadService::pauseSubscription()` returns early unless the current status is `active`, and the controller responds 200 regardless. Pausing an already-paused, `past_due` or `cancelled` subscription looks identical to success — confirm by reading `data.status` in the response.
- The pause is always open-ended. The API passes no until-date, so `paused_until` stays null and nothing auto-resumes; call `/resume` explicitly. (`paused_until` is not in the response either, so a dashboard-scheduled pause is invisible here.)

## Resume a subscription

`POST /v1/subscriptions/{uuid}/resume`

Returns a paused or past-due subscription to `active`, clearing the failed-charge counter and the last decline reason. Takes no body.

- **Operation id:** `subscriptions.resume`
- **Required ability:** `subscriptions:write`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Request

```bash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/resume \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

#### Responses

##### 200 — Active again. `failed_charge_count` is reset and a lapsed `next_charge_at` has been pushed one interval into the future.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "active",
    "cadence": "monthly",
    "cadence_label": "Monthly",
    "discount_pct": 10,
    "next_charge_at": "2026-09-11T09:15:44+00:00",
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-11T09:15:44+00:00"
  }
}
```

##### 404 — No such subscription for the token's team.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- **Silent no-op outside `paused` and `past_due`.** Resuming an already-active subscription changes nothing, and `cancelled` is terminal for this endpoint — v1 exposes no way to revive a cancelled subscription, which is a dashboard-only action.
- If `next_charge_at` is null or already in the past, it is rewritten to now plus one cadence interval, so a long-paused subscription is not immediately billed for the cycles it missed.
- Resuming also zeroes `failed_charge_count` and clears the internal decline type — dunning starts over from a clean slate.

## Skip the next cycle

`POST /v1/subscriptions/{uuid}/skip`

Pushes `next_charge_at` forward by exactly one cadence interval, skipping a single shipment without changing the subscription's status. Takes no body.

- **Operation id:** `subscriptions.skip`
- **Required ability:** `subscriptions:write`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Request

```bash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/skip \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

#### Responses

##### 200 — `next_charge_at` has moved from 2026-09-01 to 2026-10-01. Status is unchanged.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "active",
    "cadence": "monthly",
    "cadence_label": "Monthly",
    "discount_pct": 10,
    "next_charge_at": "2026-10-01T14:00:00+00:00",
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-11T09:15:44+00:00"
  }
}
```

##### 404 — No such subscription for the token's team.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- **Silent no-op outside `active`.** A paused, past-due or cancelled subscription returns 200 with an unchanged `next_charge_at`. Compare the returned date against the one you sent from.
- Skips stack. The new date is computed from the *existing* `next_charge_at`, so calling this twice defers two cycles. There is no un-skip; correcting an accidental double skip means moving the date from the dashboard.
- The offset is calendar-month arithmetic (1, 3, 6 or 12 months), not a fixed number of days, and it preserves the time of day.

## Cancel a subscription

`POST /v1/subscriptions/{uuid}/cancel`

Ends the subscription. Status becomes `cancelled`, `next_charge_at` is cleared, and the cancellation timestamp and optional reason are recorded against the audit trail.

- **Operation id:** `subscriptions.cancel`
- **Required ability:** `subscriptions:write`
- **Rate limit:** 60 requests/minute per team

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | The subscription's `uuid`. Example: `9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31` |

#### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `reason` | string | no | Free-text cancellation reason stored on the subscription and its audit event. Keep it under 255 characters — the column is `varchar(255)` and the value is not validated before insert. Example: `Customer requested by phone` |

#### Request

```bash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/cancel \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Customer requested by phone"}'
```

#### Responses

##### 200 — Cancelled. `next_charge_at` is now null; the reason and cancellation timestamp are stored but not echoed back.

```json
{
  "data": {
    "uuid": "9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31",
    "subscription_id": 1042,
    "status": "cancelled",
    "cadence": "monthly",
    "cadence_label": "Monthly",
    "discount_pct": 10,
    "next_charge_at": null,
    "last_charged_at": "2026-08-01T14:00:00+00:00",
    "failed_charge_count": 0,
    "customer_id": 1187,
    "items": [
      {
        "product_id": 2043,
        "variant_id": 5581,
        "quantity": 2,
        "unit_price": 24.99
      }
    ],
    "shipping_address": {
      "address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
      "type": "shipping",
      "address_line1": "418 Ridgeline Dr",
      "address_line2": null,
      "city": "Bozeman",
      "state": "MT",
      "zip_code": "59718",
      "is_billing": false,
      "is_shipping": true,
      "is_default_billing": false,
      "is_default_shipping": true
    },
    "payment_method": {
      "payment_method_id": "2b9e4c61-8a05-4f7d-9c33-1e6b0d5a82f4",
      "brand": "visa",
      "last_four": "4242"
    },
    "created_at": "2026-03-14T09:22:07+00:00",
    "updated_at": "2026-08-11T09:15:44+00:00"
  }
}
```

##### 404 — No such subscription for the token's team.

```json
{
  "error": "not_found",
  "message": "Subscription not found."
}
```

#### Caveats

- **Silent no-op on an already-cancelled subscription.** Only `active`, `paused` and `past_due` are cancellable; a repeat call returns 200 and leaves the original `cancelled_at` and reason intact, so cancel is effectively idempotent but never tells you which call did the work.
- Cancellation is one-way through this API. There is no reactivate endpoint — bringing a cancelled subscription back is a dashboard action, and `/resume` will not do it.
- `reason` is read straight off the request with no validation rule. A string longer than 255 characters is rejected by the database, not by the validator, and surfaces as a 500 with the subscription left uncancelled.
- Neither `cancelled_at` nor `cancellation_reason` appears in `SubscriptionResource`, so you cannot read back what you just stored. Keep your own record if you need it.

Source: https://docs.firearmcart.com/api/resources/subscriptions/index.mdx
