# Usual for developers and agents Usual runs subscriptions for Shopify stores: offers on product pages, renewals, failed-payment recovery, cancellation offers, and moving in from other apps. Everything a merchant can do in the admin, you can do through the API, and an AI agent can do through MCP, safely. ## Two ways in | | REST API | MCP | |---|---|---| | **For** | Your code, integrations, scripts | AI agents: Claude, Cursor, ChatGPT and others | | **Address** | `https://regular-subs.fly.dev/api/v1` | `https://regular-subs.fly.dev/mcp` | | **Sign-in** | `Authorization: Bearer ` | The same key | | **Start with** | [Quickstart](/docs/quickstart) | [Connect an agent](/docs/mcp) | Both run on the same engine as the admin. A skip made through MCP is the same skip a customer makes on their account page: same checks, same activity record, same undo. ## What makes it different **Every change can be previewed first.** Any change returns a *plan*: exactly what would change and how many records it touches, before anything happens. [How changes work](/docs/concepts/changes) **Agents can't go too far on their own.** Agents always get a plan first and must say why. Riskier or bigger changes wait for a person to approve them in the admin. [Approvals](/docs/concepts/changes#approvals) **Almost everything can be undone.** Every applied change is recorded with who made it, why, and how to reverse it. [Undo](/docs/concepts/changes#undo) **Retries are safe.** Send an `Idempotency-Key` and a retried request returns the original result instead of repeating the change. [Errors and retries](/docs/concepts/errors) **Customers only ever see their own data.** Each store's data is walled off in the database, and customer-facing access is limited to that customer's own subscriptions. [Data and privacy](/docs/concepts/data) ## Where to next - [Quickstart](/docs/quickstart): your first call in three minutes - [Connect an agent](/docs/mcp): add Usual to Claude, Cursor or another MCP client - [Recipes](/docs/recipes/cancel-request): handle a cancel request, fix failed payments, move a store from Recharge - [API reference](/docs/reference/api), [Commands](/docs/reference/commands), [MCP tools](/docs/reference/mcp-tools) For AI agents reading this: [`/llms.txt`](https://regular-subs.fly.dev/llms.txt) lists every page, and [`/llms-full.txt`](https://regular-subs.fly.dev/llms-full.txt) has all of them in one file. Every page is also available as Markdown: swap `/docs/` for `/docs/md/` in its address. --- # Quickstart Make your first call in three minutes: create a key, read the store, then preview and make a change. ## 1. Create a key In Shopify admin, open **Usual → Developers → Create key**. Give it a name, choose **API key** (for your code) or **Agent** (for an AI agent), and pick only the permissions it needs. Copy the key: it's shown once. ```bash export USUAL_KEY="usk_..." ``` Keys look like `usk_` followed by two random parts. We store only a hash of them. Revoke a key any time from the same page. ## 2. Read something Start with the overview: whether everything is working, active subscribers, monthly recurring revenue and what's due this week. ```bash curl https://regular-subs.fly.dev/api/v1/overview \ -H "Authorization: Bearer $USUAL_KEY" ``` Then find subscriptions. Filters combine: ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions?status=ACTIVE&nextBillingTo=2026-12-31T00:00:00Z&limit=10" \ -H "Authorization: Bearer $USUAL_KEY" ``` ```json { "subscriptions": [ { "id": "cmw5ttqtk000axbrkeltlbrm5", "status": "ACTIVE", "nextBillingDate": "2026-12-15T14:00:00.000Z", "billing": { "every": 1, "interval": "MONTH" }, "lines": [{ "title": "House Blend Coffee", "quantity": 2, "unitPrice": { "amount": "18.50", "currency": "USD" } }], "recurringTotal": { "amount": "41.99", "currency": "USD" } } ], "total": 1, "nextCursor": null } ``` ## 3. Preview a change Add `?preview=true` to any change to see exactly what it would do, without doing it: ```bash curl -X POST "https://regular-subs.fly.dev/api/v1/subscriptions/cmw5ttqtk000axbrkeltlbrm5/skip?preview=true" \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```json { "status": "planned", "plan": { "id": "cmw5u2...", "command": "subscriptions.skip_next", "diff": { "summary": "Skip the delivery on 2026-12-15; next one 2027-01-15.", "changes": [...] }, "affected": 1, "status": "pending", "policy": { "outcome": "allow", "reasons": [] }, "expiresAt": "2026-11-07T15:15:00.000Z", "nextStep": "Review the diff, then apply this plan by its id. It expires at expiresAt." } } ``` ## 4. Make it Apply the plan exactly as previewed. If anything changed in the meantime, it's refused instead of doing something you didn't see: ```bash curl -X POST https://regular-subs.fly.dev/api/v1/plans/cmw5u2.../apply \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Idempotency-Key: skip-cmw5ttqtk-2026-12" ``` Or skip the preview and make the change directly: the same request without `?preview=true`. Either way it's recorded in the activity log with your key's name, and it can be undone. ## Next - [How changes work](/docs/concepts/changes): plans, approvals and undo - [Connect an agent](/docs/mcp): the same, for Claude or Cursor - [API reference](/docs/reference/api) and [Commands](/docs/reference/commands): everything you can call --- # Usual Subagent **Usual Subagent** is how your own AI agent runs your subscriptions: an MCP server built into Usual. Connect Claude, Cursor or any MCP client and your agent can look up subscriptions, explain what happened, handle a customer's cancel request, fix failed payments, and make changes, each one previewed first and recorded. | | | |---|---| | **Address** | `https://regular-subs.fly.dev/mcp` (Streamable HTTP) | | **Sign-in** | An **Agent** key from **Usual → Developers**, sent as `Authorization: Bearer ` | Use an *Agent* key rather than an *API key*: agent keys always get a plan first, must say why, and need a person's approval for riskier or bigger changes (see [Approvals](/docs/concepts/changes#approvals)). Give it only the permissions the job needs. ## Claude Code ```bash claude mcp add --transport http usual https://regular-subs.fly.dev/mcp \ --header "Authorization: Bearer $USUAL_KEY" ``` ## Cursor In `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "usual": { "url": "https://regular-subs.fly.dev/mcp", "headers": { "Authorization": "Bearer usk_..." } } } } ``` ## VS Code In `.vscode/mcp.json`: ```json { "servers": { "usual": { "type": "http", "url": "https://regular-subs.fly.dev/mcp", "headers": { "Authorization": "Bearer ${input:usual-key}" } } }, "inputs": [{ "id": "usual-key", "type": "promptString", "description": "Usual agent key", "password": true }] } ``` ## Claude Desktop and other clients Clients that only run local servers can connect through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote): ```json { "mcpServers": { "usual": { "command": "npx", "args": ["mcp-remote", "https://regular-subs.fly.dev/mcp", "--header", "Authorization: Bearer ${USUAL_KEY}"], "env": { "USUAL_KEY": "usk_..." } } } } ``` Connecting from Claude.ai or ChatGPT with a "sign in to your store" button is coming soon. ## What your agent sees The server tells the agent how to work when it connects, so you don't need to explain it. In short: 1. **Look things up** with `get_overview`, `find_subscriptions` and `get_subscription`. 2. **See what it may change** with `list_commands`: every command its key allows, with the input schema, risk and whether it can be undone. 3. **Preview** with `plan_change`. Nothing changes yet. The plan says exactly what would change. 4. **Apply** with `apply_plan`, after showing you the plan if you're there. If it needs approval, it tells you and waits; you approve it in **Usual → Activity**. 5. **Explain or reverse** with `list_activity` and `undo_change`. All the tools are listed in [MCP tools](/docs/reference/mcp-tools). ## Try asking - "How are subscriptions doing this month? Anything broken?" - "Which payments are failing, and why? Send card-update emails to the ones with expired cards." - "Maria wants to cancel because she has too much coffee. What can we offer her?" - "Pause every subscription for the Cold Brew Pack until the new batch arrives." - "What changed yesterday, and who did it?" - "We're moving from Recharge. Walk me through it." --- # How changes work Every change in Usual (a skip, a new offer, a cancellation, a setting) goes through one path, whether it comes from the admin, a customer, the API or an agent. That path is what makes it safe to hand to software. ## Commands A *command* is one kind of change: `subscriptions.skip_next`, `offers.create`, `shop.update_settings`. Each one declares: - **its input**, as a JSON schema, checked before anything happens; - **who may run it**: staff, API keys, agents, customers; - **the permission it needs**, like `subscriptions:write`; - **its risk**: `low`, `medium`, `high` or `financial`; - **whether it can be undone**. `GET /commands` (or `list_commands` in MCP) returns every command your key may run, with all of the above. The full list is in [Commands](/docs/reference/commands). ## Plans Before a command changes anything, it works out a *plan*: a plain-language summary, the exact fields that would change from what to what, and how many records it touches. ```json { "diff": { "summary": "Deliver every 2 weeks instead of every month. The next charge date stays the same.", "changes": [{ "path": "billing", "from": { "every": 1, "interval": "MONTH" }, "to": { "every": 2, "interval": "WEEK" } }] }, "affected": 1 } ``` You get a plan back when you: - add `?preview=true` to a command's endpoint, or call `POST /plans`; - use an **agent** key: agents always get a plan first, whatever they ask for; - make a change that needs a person's approval. Apply it with `POST /plans/{id}/apply`. Plans are applied **exactly as previewed**: if the subscription changed since (say the customer skipped in the meantime), the plan is refused as `plan_stale` instead of doing something nobody saw. Plans expire after 15 minutes; make a new one. If nothing would change, you get `"status": "noop"` and nothing is recorded. ## Approvals Some changes wait for a person to approve them in **Usual → Activity**. Built in, and merchants can only make these stricter: | Who | Needs approval for | |---|---| | Agents | Any `medium`, `high` or `financial` risk change, or any change touching more than 25 records | | API keys | `financial` changes, like retrying a charge | | Staff in the admin | Nothing: they're the approvers | A plan waiting for approval has `"status": "awaiting_approval"` and lasts 72 hours. Check it with `GET /plans/{id}`; once approved, apply it as usual. Agents must also say **why** with every change (`X-Change-Reason` header, or `reason` in MCP). It's recorded with the change and shown to the person approving it. ## Undo Every applied change is recorded in the activity log: what changed, from what to what, who did it (and for agents, on whose behalf), why, and when. `GET /activity` lists them. If a command is reversible, its record carries its own inverse. `POST /activity/{id}/undo` runs that inverse as a new change, through the same checks (so an agent's undo is a plan too). A change can only be undone once. ## Who did it Each change records one of these as its actor, and it's shown everywhere the change appears: | Actor | Is | |---|---| | `merchant_user` | Someone in the store's Shopify admin | | `api_key` | An integration using an API key | | `merchant_agent` | An AI agent using an agent key | | `customer` | The subscriber, from their account page or the store's portal | | `system` | Usual itself: renewals, retries, syncing from Shopify | --- # Keys and permissions Every request carries a key, sent as `Authorization: Bearer `. Keys are created in **Usual → Developers** by someone in the store's admin, and belong to that one store. ## API keys and agent keys | | API key | Agent key | |---|---|---| | **For** | Your code and integrations | AI agents | | **Changes** | Applied directly, unless they need approval | Always a plan first | | **Must say why** | No (but you can, with `X-Change-Reason`) | Yes | | **Needs approval for** | Financial changes | Medium risk and up, or more than 25 records | Use an agent key for anything an AI drives, even through the REST API. It's what tells Usual to be careful. ## Permissions Give each key only what it needs. A key without a permission gets `403 forbidden` with the missing permission named. | Permission | Allows | |---|---| | `settings:read` | Read store settings | | `settings:write` | Change store settings | | `audit:read` | Read the activity log and plans | | `subscriptions:read` | Read subscriptions | | `subscriptions:write` | Skip, reschedule, pause, resume and cancel subscriptions | | `subscriptions:sync` | Re-check subscriptions against Shopify | | `offers:read` | Read subscription offers | | `offers:write` | Create and change subscription offers | | `billing:read` | Read charges, retries and billing health | | `billing:write` | Retry failed charges (always needs approval for keys and agents) | | `imports:read` | Read subscription imports from other apps | | `imports:write` | Import subscriptions from another app and start billing them | | `integrations:write` | Connect and disconnect Klaviyo | Some things only a person in the admin can do, whatever the key: creating and revoking keys, and approving plans. Agent keys also can't connect another app's account (Recharge, Klaviyo): their changes are saved as plans first, and a plan would have to keep the other app's secret. ## Keeping keys safe - Keys are shown once when created. We store only a hash. - Revoke a key from the Developers page and it stops working immediately. - Each key shows when it was last used. - Never put a key in a web page or app that customers can see. --- # Errors and retries ## Errors Errors always have the same shape: a stable `code` to branch on, a `message` for people, and often a `hint` that says what to do next. ```json { "error": { "code": "invalid_input", "message": "A payment for this subscription is going through right now.", "hint": "Try again in a minute." } } ``` | Code | Status | Means | |---|---|---| | `invalid_input` | 400 | The input or the request doesn't make sense now. `details` lists fields. | | `unauthenticated` | 401 | Missing, invalid or revoked key. | | `forbidden` | 403 | The key lacks a permission, or this kind of caller can't run this command. | | `policy_denied` | 403 | A store rule blocks this change. | | `not_found` | 404 | No such thing in this store. | | `plan_stale` | 409 | Something changed since the plan was made. Make a new plan. | | `plan_not_pending` | 409 | The plan was already applied, rejected, or is waiting for approval. | | `already_undone` | 409 | That change was already undone. | | `plan_expired` | 410 | Plans last 15 minutes (72 hours while waiting for approval). | | `idempotency_key_reused` | 422 | That `Idempotency-Key` was used for a different request. | | `not_reversible` | 422 | That change can't be undone. | | `upstream_rejected` | 422 | Shopify refused the change. The message says why. | | `internal` | 500 | Our fault. Send us the `x-request-id`. | Every response has an `x-request-id` header. Include it when you contact us. ## Retrying safely Networks fail. To retry a change without risk of making it twice, send an `Idempotency-Key` header (any unique string up to 255 characters): ```bash curl -X POST https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/pause \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Idempotency-Key: pause-sub_123-2026-11-07" \ -H "Content-Type: application/json" -d '{}' ``` If a request with that key already succeeded, you get the original response back and nothing happens again. Reusing a key with a different request is refused, so a bug can't quietly apply the wrong change. Many commands are also harmless to repeat: asking for a frequency a subscription already has returns `"status": "noop"` and changes nothing. ## Pagination Lists return a page and a `nextCursor`. Pass it back as `cursor` (or `before`, for activity) to get the next page. `null` means you've reached the end. ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions?limit=100&cursor=eyJpZCI6..." -H "Authorization: Bearer $USUAL_KEY" ``` --- # Data and privacy ## Each store's data is walled off Every store's data is separated in the database itself, not just in our code: queries made for one store can't see another store's rows, even by mistake. Keys belong to one store and only ever see that store. ## Customers see only their own When a subscriber acts, from their Shopify account page or the store's portal, they can only see and change their own subscriptions, whatever they ask for. Shopify signs who they are; we don't take their word for it. ## What we keep We keep as little about customers as we can: - **Subscriptions** mirror Shopify's: products, prices, schedule, status, and the customer's Shopify id. Names, emails and addresses stay in Shopify. - **Customer emails** are read from Shopify when an email is sent, and never stored. The email log keeps a masked address, like `m…@gmail.com`. - **Imports** from another app keep customers' details for 30 days, then delete them. - **Tokens for other services** (Recharge, Klaviyo) are encrypted, never shown again, and hidden in the activity log. - **Klaviyo**, if the store connects it, receives subscription events with the customer's email, so the store can send its own emails from there. When Shopify asks us to delete a customer's data, or a store uninstalls, we do it automatically. ## For agents Agents see what their key's permissions allow, and nothing about a customer beyond what's needed to manage their subscription. Responses don't include card details, ever: cards stay with Shopify. --- # Handle a cancel request A customer writes in: "please cancel my subscription". A support agent (human or AI) can offer one thing that might help, and cancel straight away if it doesn't. Never more than one offer, and never making cancelling harder: that's what customers expect, and what laws like California's require. ## 1. Find the subscription ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions?status=ACTIVE&customerId=gid://shopify/Customer/7208" \ -H "Authorization: Bearer $USUAL_KEY" ``` Look customers up by their Shopify id: support tools and Shopify both have it. (We don't store customers' emails, so you can't search by one.) ## 2. Ask why, then get the offer The reason decides the offer. Map what the customer said to one of: `too_much`, `too_expensive`, `taking_break`, `not_needed`, `didnt_like`, `other`. ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/save-offer?reason=too_much" \ -H "Authorization: Bearer $USUAL_KEY" ``` ```json { "offer": { "kind": "less_often", "every": 2, "interval": "MONTH", "heading": "Get it every 2 months instead?", "detail": "Same items, less often. Change it back anytime.", "accept": "Switch" } } ``` `offer` is `null` when nothing is worth offering. The store chooses which offers are allowed; a discount only appears if the store turned it on, and at most once a year per subscription. ## 3a. They take it ```bash curl -X POST https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/save \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" \ -d '{"reason": "too_much", "offer": "less_often"}' ``` Only the offer that was shown can be accepted. For a swap, add the `variantId` they chose from `offer.choices`. The save is counted in the store's retention report. ## 3b. They don't Cancel right away, with the reason: ```bash curl -X POST https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/cancel \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" \ -d '{"reason": "too_much", "comment": "Moving abroad"}' ``` The customer gets a cancellation confirmation email (if the store has it on), and nothing is charged again, including retries of a failed payment. ## Or let them do it themselves If the customer would rather choose for themselves, or you'd rather not wait for an approval, send them a link straight to the cancel step in the store's customer portal: ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/portal-link?view=cancel" \ -H "Authorization: Bearer $USUAL_KEY" ``` ```json { "url": "https://your-store.com/apps/usual/portal?subscription=sub_123&view=cancel", "note": "Safe to send to the customer. They log in to the store first, and only see their own subscriptions." } ``` They see the same single offer for their reason, then can cancel in one click. Leave out `view` for a link to the subscription itself, where they can skip, reschedule, pause or update their card. The link holds no secret, so it's fine in an email or chat. ## With an AI agent Through MCP this is `find_subscriptions`, `get_save_offer`, then `plan_change` with `subscriptions.accept_save_offer` or `subscriptions.cancel`, or `get_portal_link` to hand it to the customer. The `get_save_offer` tool tells the agent the rules: one offer, in its own words, and cancel if they say no. With an agent key, both taking an offer and cancelling wait for someone at the store to approve them in **Usual → Activity**, because they're medium and high risk. The agent can tell the customer it's been passed on and confirm once it's done. --- # Chase failed payments When a renewal payment fails, Usual retries it on a schedule (by default 1, 3, 5 and 7 days later) and, for card problems, Shopify emails the customer a secure link to update their card. This recipe finds what's still failing and nudges the right customers. ## 1. What's failing ```bash curl "https://regular-subs.fly.dev/api/v1/billing/cycles?state=retry_scheduled&limit=100" \ -H "Authorization: Bearer $USUAL_KEY" ``` Each cycle has its attempts, with Shopify's error code on each failed one: `EXPIRED_PAYMENT_METHOD`, `INSUFFICIENT_FUNDS`, `CARD_DECLINED` and so on, plus when the next retry is. `GET /billing/summary` gives the totals. ## 2. Ask for a new card where it'll help Retrying an expired card won't work; a new card will. Send the secure update link: ```bash curl -X POST https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/payment-update \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{}' ``` Shopify sends the email; nobody, including us, sees card details. It's sent at most once an hour per subscription. Usual already sends it automatically on the first card failure, so use this for follow-ups or when a customer asks. ## 3. Retry now (optional) Once the customer has updated their card, you can retry without waiting: ```bash curl -X POST "https://regular-subs.fly.dev/api/v1/billing/cycles/cyc_456/retry?preview=true" \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{}' ``` Retrying takes money, so it's a `financial` change: from an API key or an agent it always waits for someone at the store to approve it in **Usual → Activity**. ## A weekly report, with an agent Ask your agent: *"Every Monday, list the payments still failing, grouped by reason, and send card-update emails to anyone whose card expired."* It uses `list_billing_cycles` and plans `subscriptions.send_payment_update` for each, which is low risk, so it doesn't need approval. --- # Move a store from Recharge Move every active subscription from Recharge, or from Skio, Appstle, Loop, Seal or Shopify Subscriptions with an export file, keeping each one's date, price and schedule. Nobody is emailed, and nobody is charged twice. Most stores do it in an afternoon. ## Two ways in **Connect Recharge** (best): Usual reads Recharge directly, so nothing goes stale. Cards Recharge keeps at Stripe, Braintree or Authorize.net come too, and switching billing cancels each subscription in Recharge without emailing the customer. **Upload an export** from any app as CSV. We recognise each app's column names. ## 1a. Connect and fetch The store creates a Recharge API token (Tools and apps → API tokens) with read and write access to Subscriptions, and read access to Customers and Payment methods, then connects it in **Usual → Import**, or with an API key: ```bash curl -X POST https://regular-subs.fly.dev/api/v1/imports/connections \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" \ -d '{"source": "recharge", "token": "sk_..."}' curl -X POST https://regular-subs.fly.dev/api/v1/imports/connections/recharge/fetch \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{}' ``` The token is checked with Recharge, stored encrypted, and never shown or logged. ## 1b. Or upload a file ```bash curl -X POST https://regular-subs.fly.dev/api/v1/imports \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n --rawfile csv subscriptions.csv '{csv: $csv, filename: "subscriptions.csv", source: "recharge"}')" ``` ## 2. Review what was found Every subscription is checked against the store: the customer, a saved card, the products and the next charge date. Nothing is created yet. ```bash curl https://regular-subs.fly.dev/api/v1/imports/imp_123 -H "Authorization: Bearer $USUAL_KEY" ``` Wait for `status: "ready"`. `problems` groups anything that can't be imported, with examples and how to fix it: a customer not in the store, a product that's gone, a card held at Stripe that needs Stripe connected to Shopify. Fix what you can and fetch (or upload) again; subscriptions already imported are skipped. ## 3. Import Try a few first, then the rest: ```bash curl -X POST https://regular-subs.fly.dev/api/v1/imports/imp_123/start \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{"limit": 10}' ``` Each becomes a Shopify subscription with the same date, price and schedule. They don't bill yet. ## 4. Switch billing over ```bash curl -X POST https://regular-subs.fly.dev/api/v1/imports/imp_123/release-billing \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{}' ``` For a connected import, each subscription is first checked in Recharge for anything the customer changed since the import (a skip, a cancellation, a product removed), then cancelled there without emailing them, then billed here. For an uploaded file, only do this once the old app has stopped charging. If the store's Klaviyo flows email customers when a Recharge subscription is cancelled, pause those first. ## With an AI agent *"We're moving from Recharge. Walk me through it."* The `get_import` tool carries these steps, and switching billing is high risk, so it always waits for someone at the store to approve it. --- # Pause everyone on a product A product is out of stock for a month, or recalled. Pause every active subscription that includes it, then resume them when it's back. ## 1. Find them ```bash curl "https://regular-subs.fly.dev/api/v1/subscriptions?status=ACTIVE&productId=gid://shopify/Product/123&limit=100" \ -H "Authorization: Bearer $USUAL_KEY" ``` Page through with `nextCursor` until it's `null`. `total` tells you how many there are before you start. ## 2. Preview one ```bash curl -X POST "https://regular-subs.fly.dev/api/v1/subscriptions/sub_123/pause?preview=true" \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Content-Type: application/json" -d '{}' ``` A subscription with a payment going through right now, or one that's failing, can't be paused until that settles; the error says so. ## 3. Pause each one ```bash for id in $(cat subscription-ids.txt); do curl -s -X POST "https://regular-subs.fly.dev/api/v1/subscriptions/$id/pause" \ -H "Authorization: Bearer $USUAL_KEY" \ -H "Idempotency-Key: recall-2026-11-$id" \ -H "X-Change-Reason: Cold Brew Pack recall" \ -H "Content-Type: application/json" -d '{}' done ``` The `Idempotency-Key` makes the loop safe to run again if it stops halfway. The `X-Change-Reason` shows up in the activity log, so anyone looking later knows why. ## 4. Resume when it's back `POST /subscriptions/{id}/resume` for each. If a subscription's charge date passed while it was paused, its next delivery is tomorrow rather than a date in the past. Or undo each pause from the activity log. ## With an AI agent *"Pause every active subscription for the Cold Brew Pack until the new batch arrives."* Pausing is medium risk, so with an agent key each pause waits for someone at the store to approve it in **Usual → Activity**. --- # REST API Base address: `https://regular-subs.fly.dev/api/v1`. Send your key as `Authorization: Bearer `. Responses are JSON and carry an `x-request-id` header. The machine-readable version is [`/api/v1/openapi.json`](https://regular-subs.fly.dev/api/v1/openapi.json) (OpenAPI 3.1). Changes have their own endpoints, listed with the reads for the same job. The body is the change's input and path parameters fill in the rest. Add `?preview=true` to get a plan instead of applying, and `X-Change-Reason` to say why (agents must). Every change takes an `Idempotency-Key` header for safe retries. ## Start here ### List the changes a key can make `GET /api/v1/commands` Every change this key can make, with its input schema, risk and whether it can be undone. **Permission:** any key · **Returns:** `{ commands: [{ name, description, risk, reversible, input, http }] }` ### See how subscriptions are doing `GET /api/v1/overview` How the store's subscriptions are doing: whether everything is working, active subscribers, monthly recurring revenue, charges due this week, and setup progress. The best first call. **Permission:** `subscriptions:read` · **Returns:** `{ checks, metrics, trend, forecast, topProducts, activity, setup }` ## Subscriptions ### Find subscriptions `GET /api/v1/subscriptions` Find subscriptions, newest first. Filters combine. **Permission:** `subscriptions:read` · **Returns:** `{ subscriptions: Subscription[], total, nextCursor }` | Parameter | In | Type | Description | |---|---|---|---| | `status` | query | string | Comma-separated: ACTIVE, PAUSED, CANCELLED, EXPIRED, FAILED. | | `customerId` | query | string | A Shopify customer GID. | | `productId` | query | string | Has a line for this Shopify product GID. | | `variantId` | query | string | Has a line for this Shopify variant GID. | | `sellingPlanId` | query | string | Has a line on this selling plan. | | `nextBillingFrom` | query | string | Next charge on or after this time (ISO 8601). | | `nextBillingTo` | query | string | Next charge on or before this time (ISO 8601). | | `query` | query | string | Product title, SKU, or the end of a contract or customer id. | | `limit` | query | integer | Results per page, 1–100. | | `cursor` | query | string | nextCursor from the previous page. | ### Look up a subscription `GET /api/v1/subscriptions/:id` One subscription: its status, lines, prices, schedule, next charge and last payment. **Permission:** `subscriptions:read` · **Returns:** `{ subscription: Subscription }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | Our id, or the Shopify contract GID (URL-encoded). | ### See a subscription's history `GET /api/v1/subscriptions/:id/history` What happened to one subscription, newest first, with who did each thing: the customer, staff, an agent, an API key or Shopify. **Permission:** `subscriptions:read` · **Returns:** `{ subscriptionId, events: [{ at, type, by, to, reason, ... }] }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | Our id, or the Shopify contract GID. | ### Get a link for the customer to manage it `GET /api/v1/subscriptions/:id/portal-link` A link that opens this subscription in the customer portal on the store's site, or goes straight to cancelling it. Safe to send: the customer logs in first and only ever sees their own subscriptions. **Permission:** `subscriptions:read` · **Returns:** `{ subscriptionId, view, url, note }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | Our id, or the Shopify contract GID. | | `view` | query | string | manage (default) or cancel. | ### Re-check subscriptions against Shopify `POST /api/v1/subscriptions/reconcile` Re-check every subscription against Shopify and fix anything out of date. Runs in the background; check progress with get_sync_status. Safe to run any time: it only reads from Shopify and updates our copy. **Permission:** `subscriptions:sync` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#re-check-subscriptions-against-shopify) ### Skip the next delivery `POST /api/v1/subscriptions/:subscriptionId/skip` Skip the next delivery. The next charge moves on by one delivery interval (e.g. a month). Good for customers with too much product who'd otherwise cancel. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#skip-the-next-delivery) ### Move the next delivery `POST /api/v1/subscriptions/:subscriptionId/reschedule` Move the next delivery to a specific date (tomorrow to a year out). Later deliveries follow on from the new date at the usual interval. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#move-the-next-delivery) ### Pause a subscription `POST /api/v1/subscriptions/:subscriptionId/pause` Pause a subscription: no charges or deliveries until it's resumed. **Permission:** `subscriptions:write` · **Risk:** medium · **Undo:** yes · [Input](/docs/reference/commands#pause-a-subscription) ### Resume a subscription `POST /api/v1/subscriptions/:subscriptionId/resume` Resume a paused subscription. If its next delivery date has passed, the next delivery is tomorrow at the usual time. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#resume-a-subscription) ### Cancel a subscription `POST /api/v1/subscriptions/:subscriptionId/cancel` Cancel a subscription. No more charges or deliveries. Record why: it drives which save offers are worth making. Customers can always cancel in one step. **Permission:** `subscriptions:write` · **Risk:** high · **Undo:** no · [Input](/docs/reference/commands#cancel-a-subscription) ### Change a quantity `POST /api/v1/subscriptions/:subscriptionId/quantity` Change how many of an item come in each delivery. The price per item stays the same; the charge changes with the quantity. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#change-a-quantity) ### Change how often it delivers `POST /api/v1/subscriptions/:subscriptionId/frequency` Change how often deliveries come, to another frequency on the same offer (e.g. every 4 weeks instead of every 2). The next charge date stays; later ones follow the new frequency. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#change-how-often-it-delivers) ### Swap to another variant `POST /api/v1/subscriptions/:subscriptionId/variant` Switch an item to another variant of the same product (size, flavour, grind). The subscription discount carries over to the new variant's price. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** yes · [Input](/docs/reference/commands#swap-to-another-variant) ### Change the shipping address `POST /api/v1/subscriptions/:subscriptionId/address` Ship future deliveries to a new address. The shipping price and method stay as they are, and Shopify refuses an address the store doesn't ship to. The address isn't kept in the activity log. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#change-the-shipping-address) ## Cancellation offers ### Get the offer for a customer who wants to cancel `GET /api/v1/subscriptions/:id/save-offer` The one offer for a customer who wants to cancel, for their reason (skip, deliver later, less often, pause, swap, or a discount the store allows). Null when there's nothing worth offering. Share it once; if they say no, cancel. **Permission:** `subscriptions:read` · **Returns:** `{ offer: { kind, heading, detail, accept, ... } | null }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | Our id, or the Shopify contract GID. | | `reason` | query | string | too_much, too_expensive, taking_break, not_needed, didnt_like or other. | ### See how cancellation offers are doing `GET /api/v1/retention` How cancellation offers are doing: customers shown an offer, how many stayed, the save rate and monthly revenue kept, by reason and by offer. **Permission:** `subscriptions:read` · **Returns:** `{ days, shown, saved, cancelled, saveRate, revenueKept, byOffer, byReason }` | Parameter | In | Type | Description | |---|---|---|---| | `days` | query | integer | How far back to look. Default 30. | ### Keep a customer with an offer `POST /api/v1/subscriptions/:subscriptionId/save` Take the offer shown to a customer who was about to cancel (skip, deliver later, less often, pause, swap, or a discount) instead of cancelling. Get the offer first with get_save_offer for the customer's reason; only that offer can be accepted. Recorded as a save in the retention report. **Permission:** `subscriptions:write` · **Risk:** medium · **Undo:** no · [Input](/docs/reference/commands#keep-a-customer-with-an-offer) ## Offers ### List offers `GET /api/v1/offers` The subscription offers shoppers can choose on product pages: name, frequencies, discount and products. **Permission:** `offers:read` · **Returns:** `{ offers: Offer[] }` ### Look up an offer `GET /api/v1/offers/:id` One offer. **Permission:** `offers:read` · **Returns:** `{ offer: Offer }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | Our id, or the Shopify selling plan group GID. | ### Create an offer `POST /api/v1/offers` Offer subscriptions on products: a name, one or more delivery frequencies, and an optional discount. Shoppers see it on the product page and choose a frequency at checkout. **Permission:** `offers:write` · **Risk:** medium · **Undo:** yes · [Input](/docs/reference/commands#create-an-offer) ### Change an offer `PATCH /api/v1/offers/:id` Change an offer's name, description, delivery frequencies, discount or products. Existing subscriptions keep the terms they started with; changes apply to new subscriptions. **Permission:** `offers:write` · **Risk:** medium · **Undo:** yes · [Input](/docs/reference/commands#change-an-offer) ### Stop offering a subscription `DELETE /api/v1/offers/:id` Stop offering a subscription option. It disappears from product pages. Existing subscriptions keep running on the terms they started with. **Permission:** `offers:write` · **Risk:** high · **Undo:** no · [Input](/docs/reference/commands#stop-offering-a-subscription) ## Billing ### See how billing is going `GET /api/v1/billing/summary` Charges, failed payments, retries and what's due next. **Permission:** `billing:read` · **Returns:** `{ due, retrying, failed, recent, ... }` ### List charges and payment attempts `GET /api/v1/billing/cycles` Billing cycles (one per renewal) with their payment attempts, newest first. **Permission:** `billing:read` · **Returns:** `{ cycles: Cycle[], nextCursor }` | Parameter | In | Type | Description | |---|---|---|---| | `state` | query | string | Comma-separated: scheduled, charging, retry_scheduled, succeeded, exhausted, skipped, cancelled. | | `subscriptionId` | query | string | Only this subscription's cycles. | | `limit` | query | integer | Results per page, 1–100. | | `cursor` | query | string | nextCursor from the previous page. | ### Email a card update link `POST /api/v1/subscriptions/:subscriptionId/payment-update` Email the customer a secure Shopify link to update the card on this subscription. Use it after a declined or expired card, or when a customer asks. At most once an hour per subscription. **Permission:** `subscriptions:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#email-a-card-update-link) ### Retry a failed payment now `POST /api/v1/billing/cycles/:cycleId/retry` Retry a failed charge now instead of waiting for the next scheduled retry, e.g. after the customer says they've updated their card. Charges the customer's payment method. **Permission:** `billing:write` · **Risk:** financial · **Undo:** no · [Input](/docs/reference/commands#retry-a-failed-payment-now) ## Customer emails ### See which customer emails were sent `GET /api/v1/notifications` Customer emails sent, skipped or failed, newest first, with why. Recipients are masked. **Permission:** `subscriptions:read` · **Returns:** `{ notifications: [...], problems: { missingEmailAccess } }` | Parameter | In | Type | Description | |---|---|---|---| | `subscriptionId` | query | string | Only this subscription's emails. | | `limit` | query | integer | Results per page, 1–100. | ## Imports ### List imports `GET /api/v1/imports` Subscriptions brought in from another app: each upload or fetch, its status and counts. **Permission:** `imports:read` · **Returns:** `{ imports: Import[] }` ### Check an import `GET /api/v1/imports/:id` One import: problems grouped by kind with examples, warnings, a preview of what will be created, failures, and how many are waiting to bill. **Permission:** `imports:read` · **Returns:** `{ import: ImportDetail }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | The import's id. | ### Upload subscriptions to import `POST /api/v1/imports` Upload a subscriptions export from another app (Recharge, Skio, Appstle, Loop, Seal, Shopify Subscriptions or a spreadsheet). Nothing is created yet: each subscription is checked against the store (customer, saved card, products, date) in the background. Read the import to see what's ready and what needs fixing, then run imports.start. **Permission:** `imports:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#upload-subscriptions-to-import) ### Create the imported subscriptions `POST /api/v1/imports/:importId/start` Create the import's ready subscriptions in Shopify, keeping each one's next charge date, price and schedule. They're created in the background, one at a time. They don't bill until imports.release_billing, so the old app has time to be switched off. Subscriptions with problems are left out. **Permission:** `imports:write` · **Risk:** medium · **Undo:** no · [Input](/docs/reference/commands#create-the-imported-subscriptions) ### Start billing imported subscriptions `POST /api/v1/imports/:importId/release-billing` Start billing an import's subscriptions here. Run it only once the old app has stopped charging them (turned off or uninstalled), or customers will be charged twice. Any whose charge date passed while waiting move to their next date, since the old app charged them. **Permission:** `imports:write` · **Risk:** high · **Undo:** no · [Input](/docs/reference/commands#start-billing-imported-subscriptions) ### Throw away an import `DELETE /api/v1/imports/:importId` Throw away an import that hasn't created anything yet, e.g. to upload a corrected file. Imports that have created subscriptions can't be discarded. **Permission:** `imports:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#throw-away-an-import) ### Connect Recharge `POST /api/v1/imports/connections` Connect the app the store is moving from with an API token from it, so subscriptions (and cards held at Stripe, Braintree or Authorize.net) can be fetched directly, and cancelled there when billing switches over. For Recharge the token needs: read_subscriptions, read_customers, read_payment_methods, write_subscriptions. The token is stored encrypted and never shown again. **Permission:** `imports:write` · **Risk:** medium · **Undo:** no · [Input](/docs/reference/commands#connect-recharge) ### Disconnect Recharge `DELETE /api/v1/imports/connections/:source` Remove the stored API token for the app the store moved from. Delete the token in that app too. **Permission:** `imports:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#disconnect-recharge) ### Fetch subscriptions from Recharge `POST /api/v1/imports/connections/:source/fetch` Fetch every active subscription from the connected app (Recharge) and check them against the store, like an upload but always current. Nothing is created yet. Read the import, then run imports.start. **Permission:** `imports:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#fetch-subscriptions-from-recharge) ## Plans and activity ### Preview a change `POST /api/v1/plans` Preview any command without applying it. Returns exactly what would change and whether it needs a person's approval. **Permission:** any key · **Returns:** `{ status: "planned", plan: Plan }` Takes an `Idempotency-Key` header for safe retries. ### Check a preview `GET /api/v1/plans/:id` A plan: its preview, status (pending, awaiting_approval, applied, rejected, stale, expired) and what to do next. **Permission:** any key · **Returns:** `{ plan: Plan }` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | The plan's id. | ### Apply a preview `POST /api/v1/plans/:id/apply` Apply a plan exactly as previewed. Refused if anything changed since it was made. **Permission:** any key · **Returns:** `{ status: "applied", auditId, diff, result } or { status: "planned" } while it waits for approval` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | The plan's id. | Takes an `Idempotency-Key` header for safe retries. ### See every change `GET /api/v1/activity` Every change ever applied, newest first: what, who, why, and whether it can be undone. **Permission:** `audit:read` · **Returns:** `{ activity: [...], nextCursor }` | Parameter | In | Type | Description | |---|---|---|---| | `limit` | query | integer | Results per page, 1–100. | | `before` | query | string | nextCursor from the previous page. | ### Undo a change `POST /api/v1/activity/:id/undo` Reverse a change by running its recorded inverse as a new change. Agents get a plan to apply. **Permission:** any key · **Returns:** `Same as any change: applied, or a plan` | Parameter | In | Type | Description | |---|---|---|---| | `id` | path | string | The activity entry's id. | Takes an `Idempotency-Key` header for safe retries. ## Store ### Read store settings `GET /api/v1/settings` Store settings: sender name, support email, retry schedule, customer emails, cancellation offers. **Permission:** `settings:read` · **Returns:** `{ settings: Settings }` ### Check the last re-sync with Shopify `GET /api/v1/sync-runs` Recent re-checks of our data against Shopify: when, how many were checked, and how many were out of date. **Permission:** `subscriptions:read` · **Returns:** `{ runs: [...] }` ### Change store settings `PATCH /api/v1/settings` Change store-wide subscription settings: sender name, support email, the failed-payment retry schedule, what happens after the final retry, and which customer emails are sent. Only the fields you pass change. **Permission:** `settings:write` · **Risk:** medium · **Undo:** yes · [Input](/docs/reference/commands#change-store-settings) ### Connect Klaviyo `POST /api/v1/integrations/klaviyo` Send subscription events (started, cancelled, upcoming charge, order placed, payment failed...) to the store's Klaviyo account, for its own flows. The private API key needs: Accounts: Read, Events: Full, Profiles: Full. **Permission:** `integrations:write` · **Risk:** medium · **Undo:** no · [Input](/docs/reference/commands#connect-klaviyo) ### Disconnect Klaviyo `DELETE /api/v1/integrations/klaviyo` Stop sending subscription events to Klaviyo and remove the stored key. **Permission:** `integrations:write` · **Risk:** low · **Undo:** no · [Input](/docs/reference/commands#disconnect-klaviyo) --- # Commands Every change you can make. Run one through its endpoint, preview it with `POST /plans` or `?preview=true`, or from an agent with `plan_change`. `GET /commands` lists the ones your key may run, with their JSON schemas. See [How changes work](/docs/concepts/changes) for risk levels and approvals. ## Subscriptions ### Re-check subscriptions against Shopify `subscriptions.reconcile` Re-check every subscription against Shopify and fix anything out of date. Runs in the background; check progress with get_sync_status. Safe to run any time: it only reads from Shopify and updates our copy. **Risk:** low · **Undo:** no · **Permission:** `subscriptions:sync` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `POST /api/v1/subscriptions/reconcile` No input. ### Skip the next delivery `subscriptions.skip_next` Skip the next delivery. The next charge moves on by one delivery interval (e.g. a month). Good for customers with too much product who'd otherwise cancel. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/skip` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | ### Move the next delivery `subscriptions.reschedule` Move the next delivery to a specific date (tomorrow to a year out). Later deliveries follow on from the new date at the usual interval. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/reschedule` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `date` | string (date) | Yes | New next delivery date, YYYY-MM-DD in the store's timezone | ### Pause a subscription `subscriptions.pause` Pause a subscription: no charges or deliveries until it's resumed. **Risk:** medium · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/pause` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | ### Resume a subscription `subscriptions.resume` Resume a paused subscription. If its next delivery date has passed, the next delivery is tomorrow at the usual time. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/resume` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | ### Cancel a subscription `subscriptions.cancel` Cancel a subscription. No more charges or deliveries. Record why: it drives which save offers are worth making. Customers can always cancel in one step. **Risk:** high · **Undo:** no · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/cancel` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `reason` | `too_much` · `too_expensive` · `taking_break` · `not_needed` · `didnt_like` · `other` | Yes | | | `comment` | string | | | ### Keep a customer with an offer `subscriptions.accept_save_offer` Take the offer shown to a customer who was about to cancel (skip, deliver later, less often, pause, swap, or a discount) instead of cancelling. Get the offer first with get_save_offer for the customer's reason; only that offer can be accepted. Recorded as a save in the retention report. **Risk:** medium · **Undo:** no · **Permission:** `subscriptions:write` · **Who:** customers, customers' agents, staff, agents, API keys **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/save` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `reason` | `too_much` · `too_expensive` · `taking_break` · `not_needed` · `didnt_like` · `other` | Yes | | | `offer` | `skip` · `later` · `less_often` · `pause` · `swap` · `discount` | Yes | The offer's kind, from get_save_offer | | `variantId` | string (starts_with) | | For swap: the variant chosen | ### Email a card update link `subscriptions.send_payment_update` Email the customer a secure Shopify link to update the card on this subscription. Use it after a declined or expired card, or when a customer asks. At most once an hour per subscription. **Risk:** low · **Undo:** no · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/payment-update` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | ### Change a quantity `subscriptions.change_quantity` Change how many of an item come in each delivery. The price per item stays the same; the charge changes with the quantity. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/quantity` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `lineId` | string | | Which line (our line id or the Shopify line GID). Optional when there's only one. | | `quantity` | integer (1–99) | Yes | How many of this item in each delivery | ### Change how often it delivers `subscriptions.change_frequency` Change how often deliveries come, to another frequency on the same offer (e.g. every 4 weeks instead of every 2). The next charge date stays; later ones follow the new frequency. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/frequency` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `every` | integer (1–52) | Yes | Deliver every N intervals | | `interval` | `DAY` · `WEEK` · `MONTH` · `YEAR` | Yes | | ### Swap to another variant `subscriptions.swap_variant` Switch an item to another variant of the same product (size, flavour, grind). The subscription discount carries over to the new variant's price. **Risk:** low · **Undo:** yes · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/variant` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `lineId` | string | | Which line (our line id or the Shopify line GID). Optional when there's only one. | | `variantId` | string (starts_with) | Yes | Another variant of the same product | ### Change the shipping address `subscriptions.change_address` Ship future deliveries to a new address. The shipping price and method stay as they are, and Shopify refuses an address the store doesn't ship to. The address isn't kept in the activity log. **Risk:** low · **Undo:** no · **Permission:** `subscriptions:write` · **Who:** staff, API keys, agents, built-in agents, customers **Endpoint:** `POST /api/v1/subscriptions/:subscriptionId/address` | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | Our subscription id, or the Shopify contract GID | | `address` | object | Yes | Where future deliveries should go | | `address.firstName` | string | | | | `address.lastName` | string | | | | `address.company` | string | | | | `address.address1` | string | Yes | | | `address.address2` | string | | | | `address.city` | string | Yes | | | `address.provinceCode` | string | | Province or state code, e.g. ON or CA | | `address.countryCode` | string | Yes | Two-letter country code, e.g. CA | | `address.zip` | string | | | | `address.phone` | string | | | ## Offers ### Create an offer `offers.create` Offer subscriptions on products: a name, one or more delivery frequencies, and an optional discount. Shoppers see it on the product page and choose a frequency at checkout. **Risk:** medium · **Undo:** yes · **Permission:** `offers:write` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `POST /api/v1/offers` | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Shown to shoppers, e.g. "Subscribe & save" | | `description` | string | | | | `frequencies` | object[] | Yes | Delivery options shoppers can choose from | | `discount` | any | Yes | Applied to every subscription order; null for no discount | | `productIds` | string (starts_with)[] | Yes | | ### Change an offer `offers.update` Change an offer's name, description, delivery frequencies, discount or products. Existing subscriptions keep the terms they started with; changes apply to new subscriptions. **Risk:** medium · **Undo:** yes · **Permission:** `offers:write` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `PATCH /api/v1/offers/:id` | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | | `name` | string | | | | `description` | string | | | | `frequencies` | object[] | | | | `discount` | any | | | | `productIds` | string (starts_with)[] | | The full set of products; replaces the current set | ### Stop offering a subscription `offers.delete` Stop offering a subscription option. It disappears from product pages. Existing subscriptions keep running on the terms they started with. **Risk:** high · **Undo:** no · **Permission:** `offers:write` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `DELETE /api/v1/offers/:id` | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | ## Billing ### Retry a failed payment now `billing.retry_now` Retry a failed charge now instead of waiting for the next scheduled retry, e.g. after the customer says they've updated their card. Charges the customer's payment method. **Risk:** financial · **Undo:** no · **Permission:** `billing:write` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `POST /api/v1/billing/cycles/:cycleId/retry` | Field | Type | Required | Description | |---|---|---|---| | `cycleId` | string | Yes | | ## Imports ### Upload subscriptions to import `imports.create` Upload a subscriptions export from another app (Recharge, Skio, Appstle, Loop, Seal, Shopify Subscriptions or a spreadsheet). Nothing is created yet: each subscription is checked against the store (customer, saved card, products, date) in the background. Read the import to see what's ready and what needs fixing, then run imports.start. **Risk:** low · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `POST /api/v1/imports` | Field | Type | Required | Description | |---|---|---|---| | `csv` | string | Yes | The export from the other app, as CSV text, header row first | | `filename` | string | | | | `source` | `recharge` · `skio` · `appstle` · `loop` · `seal` · `shopify` · `other` | | Which app it came from. Guessed from the columns when left out. | ### Create the imported subscriptions `imports.start` Create the import's ready subscriptions in Shopify, keeping each one's next charge date, price and schedule. They're created in the background, one at a time. They don't bill until imports.release_billing, so the old app has time to be switched off. Subscriptions with problems are left out. **Risk:** medium · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `POST /api/v1/imports/:importId/start` | Field | Type | Required | Description | |---|---|---|---| | `importId` | string | Yes | The import's id, from imports.create | | `limit` | integer (1–100000) | | Import only this many (e.g. 10) to check them in Shopify first. The rest stay ready. | ### Start billing imported subscriptions `imports.release_billing` Start billing an import's subscriptions here. Run it only once the old app has stopped charging them (turned off or uninstalled), or customers will be charged twice. Any whose charge date passed while waiting move to their next date, since the old app charged them. **Risk:** high · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `POST /api/v1/imports/:importId/release-billing` | Field | Type | Required | Description | |---|---|---|---| | `importId` | string | Yes | The import's id, from imports.create | ### Throw away an import `imports.discard` Throw away an import that hasn't created anything yet, e.g. to upload a corrected file. Imports that have created subscriptions can't be discarded. **Risk:** low · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `DELETE /api/v1/imports/:importId` | Field | Type | Required | Description | |---|---|---|---| | `importId` | string | Yes | The import's id, from imports.create | ### Connect Recharge `imports.connect` Connect the app the store is moving from with an API token from it, so subscriptions (and cards held at Stripe, Braintree or Authorize.net) can be fetched directly, and cancelled there when billing switches over. For Recharge the token needs: read_subscriptions, read_customers, read_payment_methods, write_subscriptions. The token is stored encrypted and never shown again. **Risk:** medium · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys **Endpoint:** `POST /api/v1/imports/connections` Contains a secret (`token`), so it's hidden in the activity log and can't wait as a plan. | Field | Type | Required | Description | |---|---|---|---| | `source` | `recharge` | Yes | The app to connect. Recharge for now. | | `token` | string | Yes | An API token created in that app | ### Disconnect Recharge `imports.disconnect` Remove the stored API token for the app the store moved from. Delete the token in that app too. **Risk:** low · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `DELETE /api/v1/imports/connections/:source` | Field | Type | Required | Description | |---|---|---|---| | `source` | `recharge` | Yes | | ### Fetch subscriptions from Recharge `imports.fetch` Fetch every active subscription from the connected app (Recharge) and check them against the store, like an upload but always current. Nothing is created yet. Read the import, then run imports.start. **Risk:** low · **Undo:** no · **Permission:** `imports:write` · **Who:** staff, API keys, agents **Endpoint:** `POST /api/v1/imports/connections/:source/fetch` | Field | Type | Required | Description | |---|---|---|---| | `source` | `recharge` | Yes | | ## Integrations ### Connect Klaviyo `integrations.connect_klaviyo` Send subscription events (started, cancelled, upcoming charge, order placed, payment failed...) to the store's Klaviyo account, for its own flows. The private API key needs: Accounts: Read, Events: Full, Profiles: Full. **Risk:** medium · **Undo:** no · **Permission:** `integrations:write` · **Who:** staff, API keys **Endpoint:** `POST /api/v1/integrations/klaviyo` Contains a secret (`apiKey`), so it's hidden in the activity log and can't wait as a plan. | Field | Type | Required | Description | |---|---|---|---| | `apiKey` | string | Yes | A Klaviyo private API key (pk_...) | ### Disconnect Klaviyo `integrations.disconnect_klaviyo` Stop sending subscription events to Klaviyo and remove the stored key. **Risk:** low · **Undo:** no · **Permission:** `integrations:write` · **Who:** staff, API keys, agents **Endpoint:** `DELETE /api/v1/integrations/klaviyo` No input. ## Store ### Change store settings `shop.update_settings` Change store-wide subscription settings: sender name, support email, the failed-payment retry schedule, what happens after the final retry, and which customer emails are sent. Only the fields you pass change. **Risk:** medium · **Undo:** yes · **Permission:** `settings:write` · **Who:** staff, API keys, agents, built-in agents **Endpoint:** `PATCH /api/v1/settings` | Field | Type | Required | Description | |---|---|---|---| | `senderName` | string | | Name shown on subscriber emails and texts. | | `supportEmail` | string (email) | | Where subscriber replies go. null to use the store's contact email. | | `accountPageUrl` | string (uri) | | The store's Subscriptions page in customer accounts, copied from the checkout and accounts editor. Customer emails and links go there. null to use the customer account home. | | `dunningRetryDays` | integer[] | | Days after a failed charge to retry it, in order. | | `afterFinalRetry` | `pause` · `cancel` | | What happens to a subscription when every retry has failed. | | `billingPaused` | boolean | | Stop all new charges for this store. Charges already sent still finish. | | `notices` | object | | Which customer emails Usual sends. Turn off any you'd rather send yourself. | | `notices.confirmation` | boolean | Yes | Email subscribers their terms and how to cancel when they sign up. | | `notices.upcomingCharge` | boolean | Yes | Remind subscribers before each renewal charge. | | `notices.upcomingChargeDays` | integer (1–14) | Yes | How many days before a charge to remind them. | | `notices.paymentFailed` | boolean | Yes | Tell subscribers when a renewal payment fails. | | `notices.cancelled` | boolean | Yes | Confirm cancellations by email. | | `retention` | object | | What customers are offered when they go to cancel. | | `retention.offers` | object | Yes | Which offers customers can see when they cancel. One fits their reason; cancelling stays one click. | | `retention.offers.skip` | boolean | Yes | Skip the next delivery instead. | | `retention.offers.later` | boolean | Yes | Get the next delivery two weeks later. | | `retention.offers.lessOften` | boolean | Yes | Switch to a less frequent schedule from the subscription's offer. | | `retention.offers.pause` | boolean | Yes | Pause until they resume. | | `retention.offers.swap` | boolean | Yes | Swap to another option of the same product. | | `retention.offers.discount` | boolean | Yes | A discount on the next few deliveries. | | `retention.discountPercent` | integer (5–50) | Yes | The discount, in percent. | | `retention.discountDeliveries` | integer (1–6) | Yes | How many deliveries the discount lasts. | ## Keys ### Create a key `api_client.create` Create an API key or agent credential. The token is returned once and never shown again. **Risk:** high · **Undo:** no · **Permission:** `*` · **Who:** staff | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | | | `kind` | `api_key` · `agent` | Yes | agent: an AI agent. Its writes always need a preview first, and riskier ones need approval. | | `scopes` | `settings:read` · `settings:write` · `audit:read` · `subscriptions:read` · `subscriptions:write` · `subscriptions:sync` · `offers:read` · `offers:write` · `billing:read` · `billing:write` · `imports:read` · `imports:write` · `integrations:write`[] | Yes | | ### Revoke a key `api_client.revoke` Revoke an API key or agent credential. It stops working immediately. **Risk:** high · **Undo:** no · **Permission:** `*` · **Who:** staff | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | --- # MCP tools Connect to `https://regular-subs.fly.dev/mcp` (Streamable HTTP) with an agent key; see [Connect an agent](/docs/mcp). Changes go through `plan_change` and `apply_plan` with any command from [Commands](/docs/reference/commands). ## What the server tells agents Sent to every agent when it connects: ```text You are connected to Usual Subagent: the agent connection to a Shopify store's subscriptions in Usual. To look things up, use find_subscriptions (filters by status, product, customer, next charge date) and get_subscription. Subscription data mirrors Shopify and is kept current automatically; get_sync_status shows the last re-check. How to make changes: 1. Call list_commands to see what you can do and each command's input schema. 2. Call plan_change with the command, its input and a short reason. Nothing changes yet. 3. Show the plan's diff to the user if they're present, then call apply_plan with the plan id. Plans expire after 15 minutes and are refused if anything changed since they were made; create a new plan if that happens. Riskier plans need a person to approve them in the app. If plan_change returns status "awaiting_approval", tell the user and check back with get_plan. Every change is recorded in list_activity and most can be undone with undo_change. ``` ## Start here ### See how subscriptions are doing `get_overview` · Read-only Start here. Whether everything is working (Shopify updates, nightly re-check, billing), active subscribers, monthly recurring revenue, charges due in 7 days, payments retrying, new and cancelled in 30 days, and what's left to set up. No input. ## Making changes ### List the changes you can make `list_commands` · Read-only Every change you're allowed to make, with a description, risk level, whether it can be undone, and the JSON schema of its input. Call this before plan_change. No input. ### Preview a change `plan_change` · Read-only Preview a command without applying it. Returns a plan with a diff of exactly what would change, how many records it touches, and whether a person must approve it. Apply it with apply_plan. If nothing would change, returns status "noop". | Field | Type | Required | Description | |---|---|---|---| | `command` | string | Yes | Command name from list_commands, e.g. "shop.update_settings" | | `input` | object | Yes | The command's input, matching its schema | | `reason` | string | Yes | Why you're making this change, in one sentence | ### Apply a preview `apply_plan` · Makes changes Apply a plan from plan_change exactly as previewed. Refused if the plan expired, needs approval, or the store changed since it was made (then make a new plan). | Field | Type | Required | Description | |---|---|---|---| | `plan_id` | string | Yes | | ### Check a preview `get_plan` · Read-only Current status of a plan, e.g. to see whether a person has approved it. | Field | Type | Required | Description | |---|---|---|---| | `plan_id` | string | Yes | | ### See every change `list_activity` · Read-only Every applied change, newest first: who made it (person, integration or agent), why, what changed, and whether it can be undone. Use it to explain what happened. | Field | Type | Required | Description | |---|---|---|---| | `limit` | integer (1–100) | | | | `before` | string | | nextCursor from a previous call, for older entries | ### Undo a change `undo_change` · Read-only Reverse an earlier change from list_activity. Returns a plan to review and apply, like plan_change. | Field | Type | Required | Description | |---|---|---|---| | `activity_id` | string | Yes | | | `reason` | string | Yes | | ## Subscriptions ### Find subscriptions `find_subscriptions` · Read-only Search subscriptions, newest first. Combine filters, e.g. active subscriptions for a product charging next week. Returns lines, prices, schedule and status, plus total and nextCursor for paging. | Field | Type | Required | Description | |---|---|---|---| | `status` | `ACTIVE` · `PAUSED` · `CANCELLED` · `EXPIRED` · `FAILED`[] | | Any of these statuses | | `customerId` | string | | Shopify customer GID | | `productId` | string | | Has a line for this Shopify product GID | | `variantId` | string | | Has a line for this Shopify variant GID | | `sellingPlanId` | string | | | | `nextBillingFrom` | string (date-time) | | Next charge on or after (ISO 8601) | | `nextBillingTo` | string (date-time) | | Next charge on or before (ISO 8601) | | `query` | string | | Search product title, SKU, or the end of a contract or customer id | | `limit` | integer (1–100) | | | | `cursor` | string | | nextCursor from the previous page | ### Look up a subscription `get_subscription` · Read-only One subscription in full, by our id or its Shopify contract GID. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | ### See a subscription's history `get_subscription_history` · Read-only What happened to one subscription, newest first: created, skips, reschedules, pauses, cancellations (with reason), charges and failed payments, and who did each (customer, merchant_user, agent, api_key, shopify, usual). | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | ### Get a link for the customer to manage it `get_portal_link` · Read-only A link that opens one subscription in the store's customer portal, where the customer can skip, reschedule, pause, update their card or cancel themselves. view=cancel goes straight to cancelling (they still see the one offer for their reason). Safe to send: they log in first and only see their own subscriptions. Use it when a customer would rather do it themselves, or when you can't make the change for them. | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | | | `view` | `manage` · `cancel` | | | ## Cancellation offers ### Get the offer for a customer who wants to cancel `get_save_offer` · Read-only When a customer asks to cancel, ask why, then get the one offer that fits their reason (skip, deliver later, less often, pause, swap, or a discount the store allows). Share it once, in your words. If they take it, plan subscriptions.accept_save_offer; if not, plan subscriptions.cancel right away. Never make cancelling harder. | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | Yes | | | `reason` | `too_much` · `too_expensive` · `taking_break` · `not_needed` · `didnt_like` · `other` | Yes | | ### See how cancellation offers are doing `get_retention` · Read-only Over the last N days (default 30): customers shown an offer, how many stayed, the save rate, monthly revenue kept, and saves and cancellations by reason and by offer. | Field | Type | Required | Description | |---|---|---|---| | `days` | integer (1–365) | | | ## Offers ### List offers `list_offers` · Read-only Subscription offers shoppers can choose on product pages: name, delivery frequencies, discount and products. Change them with plan_change and the offers.* commands. No input. ### Look up an offer `get_offer` · Read-only One offer, by our id or its Shopify selling plan group GID. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | ## Billing ### See how billing is going `get_billing_summary` · Read-only Charges and failures in the last 24 hours, charges waiting to retry or on the customer (3D Secure), subscriptions due in the next 7 days, and whether billing is paused. No input. ### List charges and payment attempts `list_billing_cycles` · Read-only Each subscription charge (one per billing period) with its attempts and errors. Filter by state, e.g. retry_scheduled for failing payments, or by subscription. To retry one now, plan the billing.retry_now command (always needs a person's approval). | Field | Type | Required | Description | |---|---|---|---| | `state` | `scheduled` · `charging` · `retry_scheduled` · `succeeded` · `exhausted` · `skipped` · `cancelled`[] | | | | `subscriptionId` | string | | | | `limit` | integer (1–100) | | | | `cursor` | string | | | ## Customer emails ### See which customer emails were sent `list_notifications` · Read-only Customer emails Usual sent, skipped or couldn't send (confirmation, renewal reminder, payment failed, cancellation), newest first, with the reason when one wasn't sent. Recipients are masked. Pass subscriptionId for one subscription. | Field | Type | Required | Description | |---|---|---|---| | `subscriptionId` | string | | | | `limit` | integer (1–100) | | | ## Imports ### List imports `list_imports` · Read-only Subscriptions brought in from another app (Recharge, Skio, Appstle...): each upload, its status (checking, ready, importing, imported, released) and how many are ready, need fixing, or were imported. No input. ### Check an import `get_import` · Read-only One import: problems grouped by kind with example rows (fix them in the file and upload again), warnings, a preview of what will be created, failures, and how many are waiting to bill. Moving a store from Recharge: once the merchant has connected it (imports.connect, done in the admin), plan imports.fetch; otherwise plan imports.create with a CSV export. Wait for status ready, review this, plan imports.start, then imports.release_billing. For a connected import that switch cancels each one in Recharge quietly; for an upload, only run it once the old app has stopped charging. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | | ## Store ### Check the last re-sync with Shopify `get_sync_status` · Read-only Recent re-checks of our subscription data against Shopify: when, how many checked, and how many were out of date. To start one, plan the subscriptions.reconcile command. No input. ### Read store settings `get_settings` · Read-only Store-wide settings: sender name, support email, the failed-payment retry schedule, which customer emails are sent, and which cancellation offers customers see. No input.