Gentic Subscriptions — Documentation
Connect any AI agent to your Shopify subscriptions. Sync your Loop subscription data — subscriptions, orders, billing attempts, and cancellation reasons — into your organization's Brain, then ask in natural language: 'what's my active subscriber count?', 'why are people cancelling?', 'how much recurring revenue is at risk from failed payments this week?'. Bounded, resumable syncs keep the picture current without re-reading everything.
1. Getting Started
Sign Up & Get Your API Key
Before you can use Gentic Subscriptions, you need an API key to authenticate your requests.
- Go to gentic.co/subscriptions and create an account.
- Create an organization from your dashboard. API keys and billing are scoped to the organization.
- Generate an API key and use it as a Bearer token in your MCP client.
2. Connecting to the MCP Server
The server is available at https://mcp.gentic.co/subscriptions. For Claude Code:
claude mcp add gentic-subscriptions \
--transport http \
https://mcp.gentic.co/subscriptions \
--header "Authorization: Bearer YOUR_API_KEY"For Claude Web and ChatGPT you can also connect via OAuth — no API key needed. See the connect section on the landing page for other MCP clients (n8n, OpenClaw).
3. Agent Skill
For the best results, pair the MCP server with the Gentic Subscriptions agent skill. The MCP server gives your agent tool access; the skill teaches it the optimal workflow order. Both the raw SKILL.md and a ready-to-upload .skill bundle are generated on demand from the live manifest, so they always reflect the current tools and pricing.
Add the skill directly via URL:
https://gentic.co/subscriptions/SKILL.mdOr upload a .skill bundle to Claude Managed Agents:
https://gentic.co/subscriptions/gentic-subscriptions.skillDownload this file and upload it wherever Claude Managed Agents asks for a .skill file. It's a zip bundle generated on demand from the latest SKILL.md.
4. Tool Reference
4 tools, rendered live from the Gentic MCP manifest. Parameter tables come directly from each tool's JSON Schema.
loop_connection_status
Check whether the calling organization has connected Loop (the Shopify subscriptions app). Returns `{ connected, shop_domain, updated_at }` when connected, or `{ connected: false }` otherwise. Free. Call before loop_sync to give the user actionable guidance when the integration is missing. Never returns the API token.
This tool takes no parameters.
loop_sync
Sync subscription data from Loop (the Shopify subscriptions app) into this organization's data backend — READ-ONLY, verbatim, one BOUNDED chunk per call. Uses the org's connected Loop integration (dashboard → Integrations; no API token is passed here). Warehouses four resources into per-org tables: `loop_subscriptions`, `loop_orders`, `loop_billing_attempts`, `loop_cancellation_reasons` (dedupe-upserted on their natural key, so re-syncs refresh rows in place). BOUNDED + RESUMABLE: each call pages the requested resources only until `max_pages_per_run` pages OR `max_seconds_per_run` seconds are reached, then returns `has_more` + `next_cursor: { resource, page_no }`. The recommended flow: call again passing that `next_cursor` back as `cursor` until `has_more` is false — the walk resumes exactly where it stopped. Returns `{ synced: { <resource>: count }, has_more, next_cursor, persisted }`. INCREMENTAL SYNC — READ THIS BEFORE SETTING UP A POLL. **Page order is NOT creation order.** Measured on a 400,000-subscription store (2026-09-16): Loop's unfiltered `/subscription` page 1 returned rows created in 2023-2024, not the newest ones. So a repeating poll that starts at page 1 re-reads the same old rows forever and NEVER reaches anything created since — it will report success every time while the warehouse silently stops moving. A page number is not a time cursor. TWO TIME FILTERS, FOR TWO DIFFERENT JOBS. Both are real Loop filters on `subscriptions` (2023-10), both take ISO-8601, and Loop ANDs them if you pass both. **`created_after` — BACKFILL THE NEW ONES.** Filters on creation time, so the result set is small by construction. This is what moves a warehouse that has fallen behind: `created_after` = the newest `created_at` you already hold, then page with `cursor` until `has_more` is false. One pass, done. **`updated_after` — KEEP EVERYTHING CURRENT.** Filters on Loop's updatedAt, so one pass catches new rows AND changed ones (cancellations, swaps, pauses). This is the hourly poll: `updated_after` = the start of your last SUCCESSFUL run minus ~1h of overlap (re-upserting is free — rows dedupe on their natural key — and the overlap covers a run that died midway), then page with `cursor`. DO NOT USE `updated_after` TO BACKFILL. Every active subscription`s billing cycle bumps its updatedAt, so on a 55,000-subscription store a six-week `updated_after` matched 40,000-55,000 rows — 400-550 pages — in no useful order, with the few rows actually created in that window buried deep inside. Measured, 2026-09-17. `created_after` covers the same window in roughly 100 pages on that store (it creates 5,000-8,000 subscriptions a month), against 400-550 for `updated_after` — still a real walk, so page it with `cursor`, but a bounded one that reaches the new rows immediately instead of after hundreds of pages of 2021-2024 subscriptions. Both are `subscriptions` only; asking for either on another resource is an error, not a silently-unfiltered walk. FREE.
| Parameter | Type | Description |
|---|---|---|
resources | string[] | Subset of resources to sync this run (in order). Any of: subscriptions, orders, billing_attempts, cancellation_reasons. Omit to sync all four. |
page_size | integer | Rows per Loop page (1–100, default 50). Loop caps this at 100. 1 – 100 |
max_pages_per_run | integer | Cap on pages fetched this call (default 20, hard max 50). Keeps a single call bounded — page more via next_cursor, never a bigger run. 1 – 50 |
max_seconds_per_run | integer | Wall-clock budget in seconds for this call (default 20, hard max 30). The walk stops after the current page once exceeded; resume with next_cursor. 1 – 30 |
updated_after | string | ISO-8601 time; sync only rows UPDATED at or after it (maps to Loop's `updatedAtStartEpoch`). USE THIS FOR THE HOURLY POLL — it catches new and changed rows in one pass. Only `subscriptions` supports it on Loop's 2023-10 API; passing it with another resource is an error, never a silently-unfiltered walk. |
created_after | string | ISO-8601 time; sync only rows CREATED at or after it (maps to Loop's `createdAtStartEpoch`). USE THIS TO BACKFILL NEW SUBSCRIPTIONS. Measured on a 55,000-subscription store: `updated_after` since a date six weeks back matched 40-55k rows (400-550 pages) because every active subscription's billing cycle bumps its updatedAt, and Loop returns them in no useful order — so the handful actually created since then sit deep inside it. Filtering on creation time makes that set small by construction. Combinable with `updated_after` (Loop ANDs them). Same rules: subscriptions only. |
cursor | any | Resume point from a prior call's `next_cursor` — { resource, page_no, updated_after }. Pass it back VERBATIM; if it carries an `updated_after` it must match this call's, because a page number only means something inside the result set it was counted in. Omit/null to start from the first requested resource at page 1. |
skio_connection_status
Check whether the calling organization has connected Skio (the Shopify subscriptions app). Returns `{ connected, shop_domain, updated_at }` when connected, or `{ connected: false }` with a hint otherwise. Free. Call before skio_sync so a missing integration produces actionable guidance rather than a failed sync. Never returns the API token. Set check_schema to true to also VERIFY the connection against Skio and compare our GraphQL query against Skio's real schema — it reports, per resource, any field we request that Skio does not have (which would fail a sync outright) and any field Skio has that we do not request (which would be silently missing from the synced tables). AFTER CONNECTING A NEW SKIO ACCOUNT, call this with check_schema:true and read `available_not_selected_detail` BEFORE trusting the first data pull — a result that reports success can still have silently omitted fields, and ten field names in our query did not exist in Skio's schema when first checked against a real account. Re-run it if queries later start failing.
| Parameter | Type | Description |
|---|---|---|
check_schema | boolean | Also call Skio to confirm the key works and compare our query against Skio's actual schema. Default false — the plain check reads only stored metadata and makes no network call. |
skio_sync
Sync subscription data from Skio (the Shopify subscriptions app) into this organization's data backend — READ-ONLY, verbatim, one BOUNDED chunk per call. Uses the org's connected Skio integration (dashboard → Integrations; no API token is passed here, and there is no shop argument — a Skio token is scoped to one shop when it is minted). Warehouses four resources into per-org tables: `skio_subscriptions`, `skio_subscription_lines`, `skio_orders`, `skio_order_line_items`, upserted on their id so re-syncs refresh rows in place. BOUNDED + RESUMABLE: each call pages only until `max_pages_per_run` pages OR `max_seconds_per_run` seconds are reached, then returns `has_more` + `next_cursor: { resource, after_id, order: "id" }`. Call again passing that back as `cursor` until `has_more` is false — the walk resumes exactly where it stopped. Returns `{ synced: { <resource>: count }, has_more, next_cursor, persisted, pages_fetched }`. THE WALK IS PARENT-DRIVEN, AND THAT IS WHY IT COMPLETES. Only the parent tables are paged — `subscriptions` and `orders` — keyset on `id` (`id _gt`, ordered by `id`), with `created_since` / `updated_since` applied as plain filters on the parent. The line tables are never paged on their own: for each parent page, all of its `subscription_lines` / `order_line_items` are fetched by foreign key in one request. `resources` therefore names what gets STORED, not what gets walked — see that parameter. WHY, MEASURED ON A REAL STORE (2026-09-29). Ordering by `updatedAt` while filtering on `createdAt` made the sort key and the filter key different columns, so the walk entered the `updatedAt` index at the oldest row and rejected rows until it reached the filtered range: the FIRST page of `orders` took over 60s. The same filter ordered by `id` takes about 147ms. The line tables are worse and cannot be fixed by any filter — `order_line_items` took over 45s for one page under a bare primary-key keyset, and over 60s under any time predicate — while the same rows fetched by foreign key for 100 parents come back in about 400ms. So there is no fast way to page the line tables directly, and there is no need to. CURSORS FROM OLDER SHAPES ARE REFUSED, NOT REINTERPRETED. The `offset` and `{ after_updated_at, after_id }` shapes both encode a position that means nothing in an `id`-ordered walk; honouring one would resume somewhere arbitrary and report success. If you hold one, drop it and re-run — every resource upserts on `id`, so re-reading costs time and never duplicates. If a page times out, RAISE `page_size` rather than lowering it — see that parameter. FREE.
| Parameter | Type | Description |
|---|---|---|
resources | string[] | Which resources to PERSIST this run. Any of: subscriptions, subscription_lines, orders, order_line_items. Omit to sync all four. THIS NAMES WHAT IS STORED, NOT WHAT IS WALKED. The walk is driven by the parent tables: `subscriptions` drives `subscription_lines`, and `orders` drives `order_line_items`. Asking for `subscription_lines` alone walks Subscriptions and stores only the lines; asking for both walks Subscriptions ONCE and stores both. The `synced` counts report rows persisted per resource, so they reflect this list rather than the walk. Why: the line tables cannot be paged directly at usable speed. Measured on a real store (2026-09-29) — OrderLineItems took over 45s for one page under a bare primary-key keyset, and over 60s under any time filter — while the same rows fetched by foreign key for 100 parents come back in about 400ms. So children are always fetched per parent page, never paged on their own. NARROWING THIS LIST MEANS DROPPING `cursor`. A cursor only resumes a walk over the list that produced it; one from a wider run is rejected with an error rather than silently misapplied to the wrong resource. |
page_size | integer | Rows per Skio page (1–100, default 100). Skio caps this at 100 nodes per request. 100 is the right setting for any real walk. Pagination is KEYSET (live since #1185): the walk seeks on (updatedAt, id) rather than counting past rows, so per-request cost scales with page_size alone and does NOT grow with how deep you are. Lowering page_size is therefore pointless rather than protective — it fetches the same rows in more round trips, and it cannot fix a timeout, because depth is no longer what makes a request slow. Under the old offset pagination a deep page could exceed the 30s budget outright (measured: page_size 100 returned rows at offset 2000 and timed out at 3000); that failure mode is gone. 1 – 100 |
max_pages_per_run | integer | Cap on pages fetched this call (default 20, hard max 50). Page more via next_cursor, never via a bigger run. 1 – 50 |
max_seconds_per_run | integer | Wall-clock budget in seconds for this call (default 20, hard max 30). The walk stops after the current page once exceeded; resume with next_cursor. 1 – 30 |
cursor | any | Resume point from a prior call's `next_cursor` — `{ resource, after_id, order: "id" }`, where `resource` is always the parent driver (`subscriptions` or `orders`). Omit or null to start from the beginning. Do not hand-build one: cursors from the older offset and `(updatedAt, id)` shapes are REFUSED with an explanation rather than reinterpreted, because a position in one ordering is not a position in another and honouring it would silently skip rows while reporting success. If you get that error, drop the cursor and re-run — every resource upserts on `id`, so re-reading costs time and never duplicates. |
updated_since | any | ISO-8601 time; sync only PARENT rows updated at or after it. USE THIS FOR THE HOURLY POLL: it catches new and changed subscriptions and orders, and re-fetches all of their lines and line items by foreign key. Set it to the start of your last SUCCESSFUL run minus ~2h of overlap — re-reading is free because every resource upserts on `id`, and the overlap covers a run that died midway. KNOWN GAP, MEASURED: editing a subscription LINE does not bump the parent subscription's updatedAt. On a real store (2026-09-29), 194 of 413 lines belonging to the 300 oldest subscriptions had been updated after their parent, the worst by over 15,000 hours. So an `updated_since` poll DOES NOT catch line-only edits — swaps, quantity changes — on subscriptions whose parent row was not otherwise touched. Order line items were not affected (they are immutable once written). The cover is a periodic FULL re-walk with no filters (parents by id, children by foreign key), which is cheap because parent pages run about 140ms: schedule one daily alongside the hourly poll. Do NOT try to cover it with a time-filtered query on the line tables — every such shape measured 13-60s per page and several time out entirely. DO NOT USE THIS TO BACKFILL: billing cycles bump updatedAt on parents whose content never changed, so on a 55,000-subscription store a six-week window matched 40,000-55,000 rows on the equivalent Loop connector. Combinable with `created_since` (both are ANDed on the parent). |
created_since | any | ISO-8601 time; sync only rows CREATED at or after it (maps to Skio's `createdAt: { _gte: ... }`). USE THIS TO BACKFILL. Filtering on creation time makes the result set small by construction, so this is what moves a warehouse that has fallen behind: set it to the newest `createdAt` you already hold, then page with `cursor` until `has_more` is false. One pass, done. Combinable with `updated_since` (Skio ANDs them) — `created_since` bounds a backfill, `updated_since` keeps everything current, and they are not substitutes for each other. |
5. Pricing
Pricing is pulled live from the Gentic MCP manifest. All prices are per call and deducted from your Gentic credits.
| Tool | Cost |
|---|---|
| loop_connection_status | Free |
| loop_sync | Free |
| skio_connection_status | Free |
| skio_sync | Free |
6. Notes
- Organization-scoped: the Loop connection comes from the org's connected integration (dashboard → Integrations → Loop). No credential is ever passed to the tools.
- Cost: both `loop_connection_status` and `loop_sync` are **free**. You pay only for analytics you run over the synced data (e.g. `query_data`). Pricing is pulled live from the Gentic MCP manifest.
- `loop_sync` is READ-ONLY — it warehouses Loop data into your Brain and never writes back to Loop.
- Rows are deduped on their natural key: re-syncing refreshes the same row (mutable fields and all) rather than duplicating it.
- Syncs are bounded and resumable: each call stops at `max_pages_per_run` / `max_seconds_per_run` and returns `next_cursor`; pass it back as `cursor` to continue. Loop `has_more` to false to finish a backfill.
- The four tables — `loop_subscriptions`, `loop_orders`, `loop_billing_attempts`, `loop_cancellation_reasons` — are queried with `query_data` for all counts and aggregations.
7. When to Apply
- User wants to know their active subscriber count, MRR, or recurring-revenue trends.
- User wants to understand churn — why subscribers are cancelling, or the top cancellation reasons.
- User wants to find revenue at risk from failed payments / dunning (billing attempts).
- User wants to sync their Loop subscription data into their Brain for analysis.
- User wants to count or aggregate over subscriptions, orders, billing attempts, or cancellations.
- User wants to feed subscription metrics into a digest, retention flow, or revenue report.
8. Workflow
1. Connect Loop once, then sync without credentials
The Subscriptions server sources the Loop connection (the Admin API token) from the org's connected integration (Gentic dashboard → Integrations → Loop). You never pass credentials to the tools — connect once in the dashboard and the server reads the encrypted connection automatically. Call `loop_connection_status` first (it's free) to confirm the org is connected; if it returns `{ connected: false }`, point the user to Integrations → Loop. It never returns the API token.
2. Sync subscription data into the Brain with `loop_sync`
`loop_sync` is **free**. It is READ-ONLY and warehouses four resources into per-org tables — `loop_subscriptions`, `loop_orders`, `loop_billing_attempts`, `loop_cancellation_reasons` — dedupe-upserted on their natural key, so a re-sync refreshes existing rows in place rather than duplicating them. Returns `{ synced: { <resource>: count }, has_more, next_cursor, persisted }`.
3. Walk the data with bounded, resumable pagination
Each `loop_sync` call is BOUNDED: it pages the requested resources only until `max_pages_per_run` pages OR `max_seconds_per_run` seconds are reached, then returns `has_more` and a `next_cursor: { resource, page_no }`. To finish a large backfill, call again passing that `next_cursor` back as `cursor` until `has_more` is false — the walk resumes exactly where it stopped. Use `resources` to sync only the tables you need and `page_size` to tune batch size.
4. Count and aggregate with `query_data`
The subscription tools warehouse data; they don't analyze it. For subscriber counts, MRR, churn rate, cancellation-reason breakdowns, or failed-payment totals, use `query_data` (Gentic Data MCP) over the `loop_subscriptions`, `loop_orders`, `loop_billing_attempts`, and `loop_cancellation_reasons` tables — standard SQL GROUP BY / COUNT / SUM over the synced rows. Sync first (or re-sync incrementally) so the numbers are current.
5. Join across your other synced data
Because everything lands in the same per-org Brain, you can join subscription data with what you've synced from other Gentic servers — e.g. correlate cancellation reasons with support tickets (`support_tickets`), or subscriber cohorts with order history. Compose the question across tables with `query_data`.
6. Present results clearly
Don't dump raw JSON. Summarize the numbers — active subscribers, MRR, top cancellation reasons, revenue at risk — and cite the counts. After a sync, tell the user how many rows were `persisted` per resource and whether `has_more` is true (i.e. there's more to page through). For recurring checks, re-run `loop_sync` to refresh the tables before querying.