Skip to content

Subscriptions

Read and manage Reload recurring orders — reschedule, pause, resume, skip and cancel

Updated View as Markdown

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 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 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.
Required ability
subscriptions:read
Rate limit
60 requests/minute per team

Query parameters

statusstringoptional
Exact-match filter on subscription status. One of `active`, `paused`, `past_due`, `cancelled`.
Example: active
customer_idintegeroptional
Restrict to one customer, matched on the `customer_id` the Customers API returns — the store-scoped number, not a database key.
Example: 1187
per_pageintegeroptional
Results per page. Defaults to 25, capped at 100.
Example: 50
pageintegeroptional
1-based page number. Defaults to 1.
Example: 2
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.
Requestbash
curl "https://api.firearmcart.com/v1/subscriptions?status=active&per_page=25" \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200A page of subscriptions. The second row shows what an absent shipping address and a removed card look like.
{
  "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": "« Previous",
        "page": null,
        "active": false
      },
      {
        "url": "https://api.firearmcart.com/v1/subscriptions?page=1",
        "label": "1",
        "page": 1,
        "active": true
      },
      {
        "url": null,
        "label": "Next »",
        "page": null,
        "active": false
      }
    ],
    "path": "https://api.firearmcart.com/v1/subscriptions",
    "per_page": 25,
    "to": 2,
    "total": 2
  }
}
403The token is valid but was not granted the `subscriptions:read` ability.
{
  "message": "Invalid ability provided."
}

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`.
Required ability
subscriptions:read
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
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
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.
Requestbash
curl https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31 \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200The subscription.
{
  "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"
  }
}
404No 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.
{
  "error": "not_found",
  "message": "Subscription not found."
}

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.
Required ability
subscriptions:write
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
The subscription's `uuid`.
Example: 9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31

Body parameters

cadencestringoptional
New billing interval. One of `monthly`, `quarterly`, `semiannual`, `yearly`. Anything else is a validation error.
Example: quarterly
shipping_address_idstring (uuid)optional
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_idstring (uuid)optional
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
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.
Requestbash
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"}'
200The updated subscription. Note `next_charge_at` has been recomputed from the moment of the request, not carried over.
{
  "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"`.
{
  "success": false,
  "error": "address_not_found",
  "message": "Address not found for this subscription customer."
}
404No such subscription for the token's team.
{
  "error": "not_found",
  "message": "Subscription not found."
}

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.
Required ability
subscriptions:write
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
The subscription's `uuid`.
Example: 9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31
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.)
Requestbash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/pause \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200Paused. `next_charge_at` is retained but will not fire.
{
  "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"
  }
}
404No such subscription for the token's team.
{
  "error": "not_found",
  "message": "Subscription not found."
}

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.
Required ability
subscriptions:write
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
The subscription's `uuid`.
Example: 9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31
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.
Requestbash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/resume \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200Active again. `failed_charge_count` is reset and a lapsed `next_charge_at` has been pushed one interval into the future.
{
  "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"
  }
}
404No such subscription for the token's team.
{
  "error": "not_found",
  "message": "Subscription not found."
}

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.
Required ability
subscriptions:write
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
The subscription's `uuid`.
Example: 9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31
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.
Requestbash
curl -X POST https://api.firearmcart.com/v1/subscriptions/9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31/skip \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200`next_charge_at` has moved from 2026-09-01 to 2026-10-01. Status is unchanged.
{
  "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"
  }
}
404No such subscription for the token's team.
{
  "error": "not_found",
  "message": "Subscription not found."
}

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.
Required ability
subscriptions:write
Rate limit
60 requests/minute per team

Path parameters

uuidstringrequired
The subscription's `uuid`.
Example: 9f1c2b4e-6d3a-4f58-9c1b-2a7e5d0f8b31

Body parameters

reasonstringoptional
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
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.
Requestbash
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"}'
200Cancelled. `next_charge_at` is now null; the reason and cancellation timestamp are stored but not echoed back.
{
  "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"
  }
}
404No such subscription for the token's team.
{
  "error": "not_found",
  "message": "Subscription not found."
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close