01 · Overview
Base URL, auth, limits, errors
One base URL. One auth header. JSON in, JSON out.
https://api.gocushy.com/api/v1
Authentication
Every request carries your API key as a bearer token. Keys start with gc_ and come from your dashboard after signup. Keys are revocable per merchant.
curl https://api.gocushy.com/api/v1/me \ -H "Authorization: Bearer gc_your_key"
{
"account": {
"id": "01J2QJ3M8XN4V6T9R3E7M1B0CD",
"name": "Acme Courses",
"email": "sam@example.com",
"plan": "founding",
"stripe_connected": true
}
}Conventions
- Money is integer cents —
4900= NZ$49.00. Currency is a lowercase 3-letter ISO code. - IDs are ULIDs — 26-character sortable strings.
- Timestamps are ISO 8601.
- Send
Content-Type: application/jsonandAccept: application/json.
Rate limit
120 requests per minute per API key, across the whole /api/v1 surface. Exceed it and you get a 429. Back off and retry.
Errors
Errors are JSON, always with a human-readable message.
| Status | Shape | When |
|---|---|---|
401 | {"error": "missing_api_key" | "invalid_api_key", "message": "..."} | No bearer token, wrong prefix, or a revoked key. |
404 | {"message": "..."} | Resource doesn't exist or isn't yours — scoping never leaks other accounts' data. |
422 | {"message": "...", "errors": {"field": ["..."]}} or {"error": "slug", "message": "..."} | Validation failure, or a domain rule (e.g. not_refundable, invalid_offer). |
429 | Throttle response | Over 120 req/min. |
02 · MCP integration
The recommended path: let an agent drive
GoCushy is MCP-native. Connect the server and any MCP client — Claude Desktop, Claude Code, Cursor, ChatGPT, your own agents — gets 40+ tools that run a complete sales operation: products, offers, checkout copy, email follow-up, webhooks, affiliates, refunds. The REST API below is the same surface; MCP is just the fastest way to use it.
Local (stdio) — Claude Desktop, Claude Code, Cursor
One command, one env var. The package is @gocushy/mcp on npm.
GOCUSHY_API_KEY=gc_your_key npx -y @gocushy/mcp
Config JSON for Claude Desktop or Cursor (claude_desktop_config.json / .cursor/mcp.json):
{
"mcpServers": {
"gocushy": {
"command": "npx",
"args": ["-y", "@gocushy/mcp"],
"env": { "GOCUSHY_API_KEY": "gc_your_key" }
}
}
}Remote (Streamable HTTP) — ChatGPT and hosted agents
No install. The remote server is stateless and key-scoped per request. Three ways to authenticate:
- ChatGPT (OAuth) — add the connector with the URL
https://mcp.gocushy.com/mcp(no key in it). ChatGPT runs the OAuth handshake and shows a GoCushy consent page — paste yourgc_key there. The connection is revocable any time by revoking the key in your dashboard. - Key in the path — for clients that accept a URL-embedded key (Cursor, custom agents):
https://mcp.gocushy.com/mcp/gc_your_key - Bearer header —
https://mcp.gocushy.com/mcpwithAuthorization: Bearer gc_your_key
Step by step in ChatGPT
- Check your plan. Custom MCP connectors need Plus, Pro, Team, Business or Enterprise. They are not available on the Free plan at all — no amount of looking will surface the menu.
- Turn on Developer mode. Profile icon → Settings → Apps & Connectors → scroll to the bottom → Advanced settings → toggle Developer mode on. (On a Business/Enterprise workspace an admin does this instead, under Workspace settings → Permissions & roles. If you're on a work account and the toggle isn't there, you need your admin.)
- Create the connector. Back in Apps & Connectors a Create button now appears. Name it
GoCushyand use this URL exactly, including/mcpand with no key in it:MCP server URLhttps://mcp.gocushy.com/mcp
- Authenticate. Choose OAuth. ChatGPT runs the handshake and shows a GoCushy consent page — paste your
gc_API key there, not into ChatGPT's own fields. - Enable it in the chat. This is the step people miss. A connector being configured is not the same as it being on: open a new chat, and switch GoCushy on for that conversation. If ChatGPT says it has no GoCushy tools, this is almost always why.
ChatGPT's menus move — these steps were verified July 2026. If your screen doesn't match, search OpenAI's help centre for "developer mode MCP" rather than trusting this page, and tell us so we can fix it.
All 40+ tools
Every tool wraps a REST endpoint and returns JSON. The one thing an agent cannot do is complete Stripe identity verification — connect_payment returns a URL for the human, and that's the only human step.
| Tool | What it does | Key inputs |
|---|---|---|
connect_payment | Starts Stripe onboarding; returns the hosted URL the human must complete. | — |
payment_status | Checks whether onboarding finished and charges are enabled. | — |
create_product | Creates a sellable product. Prices in cents; subscriptions need an interval. Warns in the response if no delivery link is set. | name, price_cents, currency, type (one_time | subscription), interval (month | year), delivery_url, delivery_note |
update_product | Updates name, description, delivery link/note, or (one-time only) price. Subscription prices are immutable — create a new product instead. | product (name or ID), name, description, price_cents, delivery_url, delivery_note |
seller_defaults | Gets or sets the once-per-business answers — currency, brand accent, checkout template, standard guarantee. Applied automatically to new products and checkouts. | currency, accent, layout, guarantee (all optional; none = read) |
import_products | Imports active products from the connected Stripe account. Idempotent. | — |
list_products | Lists products with IDs and prices. | — |
create_offer | Composes a checkout: main product + optional order bump + one-click upsell. Returns a live checkout URL immediately. | name, product_id, bump_product_id + bump_headline, upsell_product_id + upsell_headline, upsell_body, success_url |
update_checkout | Sets the selling content on a checkout — headline, bullets, testimonials, guarantee, deadline, accent, and the legal_note disclosure line (shown pre-payment, repeated on the receipt). Replaces existing blocks. | offer_id, checkout (object or null) |
pause_offer | Stops an offer taking orders — its checkout link stops working immediately. Not a delete: content, stats and order history are kept, and existing subscribers keep renewing. Admin key required. | offer (name, slug, or ID) |
resume_offer | Re-opens a paused offer. Refuses with delivery_required if the offer would take money and deliver nothing — the one delivery rule that is a refusal, not a warning. Admin key required. | offer (name, slug, or ID) |
get_checkout_link | Hosted checkout URL (and short gocushy.com/l link) for an offer. | offer (name, slug, or ID) |
get_embed_code | Copy-paste snippets to include a checkout — link, lightbox popup, or inline embed. | offer, style |
list_orders | Recent orders: buyer, status, totals, bump/upsell taken. | limit (1–100, default 20) |
get_sales | Revenue, order counts, bump/upsell take rates, per-offer breakdown. | days (1–365, default 30) |
get_offer_stats | Deep-dive on one offer: net revenue, AOV, checkout completion rate, bump/upsell take rates, refund rate, daily revenue series, recent orders. | offer_id, days (1–365, default 30) |
get_attribution_report | Revenue by traffic source, campaign, or ad (Trace) — including the rebills and upsells an ad pixel never reports. Last-click by default; untracked revenue is reported separately, never guessed. Trace subscription. | days, group_by (source | campaign | ad), touch (last | first), offer_id |
get_orders_behind | The actual orders behind one attribution row — same filters and attribution as the report, so the numbers always reconcile. Trace subscription. | source, campaign, ad, days, touch |
get_roas | One line per traffic source: revenue, rebills, LTV, and ROAS where ad spend has been reported. Trace subscription. | days |
set_conversion_tracking | Sends purchases to Meta server-side (Conversions API), deduplicated against the browser pixel by order id. Call with no arguments to read the current setting; the token is write-only. Trace subscription. | meta_pixel_id, meta_capi_token |
create_campaign_link | Mints a short cushy.link carrying a whole UTM set — so email, social, and podcast traffic stops reporting as "direct". Fails if the code is taken. | code, offer or path, utm_source, utm_medium, utm_campaign, utm_content |
update_campaign_link | Re-points a live short code or fixes its tags without changing the code — a shared link never 404s in between. Tags merge. | code, plus any field to change |
list_campaign_links | Every campaign short link with its tags, destination, and click count — busiest first. | — |
report_ad_spend | Records what a network was paid for a period so ROAS is computed against real revenue. Idempotent per source + campaign + period — safe to re-run. | source, period_start, period_end, amount_cents, campaign |
connect_email | Connects one of 15 providers: mailchimp, activecampaign, kit, getresponse, drip, mailerlite, klaviyo, brevo, beehiiv, loops, flodesk, hubspot, emailoctopus, sendfox, sendy. Credentials validated live, stored encrypted; returns audiences. | provider, api_key, api_url (ActiveCampaign, Sendy), default_list_id |
connect_notifications | Chat ping on every sale, rebill, refund, failed renewal, and cancellation — Slack, Discord, or Telegram. Credentials validated live (a test message is posted), stored encrypted. Paid plan. | channel, webhook_url (Slack/Discord), bot_token + chat_id (Telegram) |
connect_paypal | Connects the merchant's own PayPal Business REST app so buyers can pay with PayPal. Validated live, stored encrypted. Paid plan. | client_id, secret, environment (live | sandbox) |
connect_wallet | Sets the merchant's own public wallet address to receive agent-paid USDC on Base (x402). Address only — never a private key. Paid plan. | wallet_address, chain |
list_followups | All follow-up rules with per-rule health (enabled, last fired, connection errors). Call before set_followup to avoid duplicates. | — |
set_followup | Maps an offer to email actions: list + tags on a trigger. | offer_id, connection_id, trigger (purchase | refund | upsell_taken | renewal | abandoned | payment_failed | dunning_recovered), list_id, tags |
update_followup | Edits a rule, or pauses it without losing its configuration. | followup_id, any of trigger, connection_id, list_id, tags, enabled |
delete_followup | Removes a rule (stops future fan-out; already-tagged buyers stay in your ESP). | followup_id |
set_business_details | Sets the merchant profile: legal name, address, tax registration, invoice prefix, tax collection. | business_name, business_address, tax_number, tax_country, invoice_prefix, support_email, collect_tax |
tax_status | Whether Stripe Tax is active on the merchant's connected account and which registrations exist. | — |
activate_tax | Activates Stripe Tax on the merchant's own connected account by setting the head-office address — Express accounts have no Stripe tax dashboard of their own. | line1, city, postal_code, country, tax_code, tax_behavior |
add_tax_registration | Registers a jurisdiction with Stripe Tax — the legal declaration that makes tax get charged there, so agents confirm with the merchant first. | country, type (standard | oss_non_union | oss_union | ioss | state_sales_tax), state |
list_tax_registrations | The merchant's Stripe Tax registrations (country + status). | — |
create_webhook | Subscribes an endpoint to order events. Returns the HMAC signing secret exactly once. | url (https), events |
verify_purchase | Checks whether an email bought from this merchant — for gating content and support. | email, product_id, offer_id |
create_coupon | Creates a promo code — percent or fixed amount off, optionally scoped to one offer, capped, or expiring. Applied server-side before tax. | code, percent_off or amount_off_cents, offer_id, max_redemptions, expires_at |
list_coupons | All coupons with discount, scope, redemption counts, expiry, and active status. | — |
deactivate_coupon | Ends a promo — the code immediately stops working at checkout. Not a delete: order history keeps the code, and it can be reactivated. Admin key required. | coupon_id |
reactivate_coupon | Turns a deactivated code back on, still subject to its original expiry and redemption cap. Admin key required. | coupon_id |
get_activity | The account's activity trail: merchant, agent, and system actions, newest first. | limit, actor (user | ai | system), action |
create_affiliate | Registers an affiliate; returns their referral code. Rates in basis points. | name, email, code, default_commission_bps, payout_note |
list_affiliates | All affiliates with referral codes and currently-due balances. | — |
get_affiliate_link | Shareable referral link for an affiliate + offer. Attribution is server-side. | affiliate_id, offer |
mark_commissions_paid | Records that the merchant sent an affiliate their released balance. Bookkeeping, not a money movement. | affiliate_id |
cancel_subscription | Cancels a buyer's subscription — at period end by default, or immediately. | order_id, immediately |
refund_order | Refunds a paid order, full or partial. Irreversible; agents are told to confirm with the merchant first. | order_id, amount_cents |
03 · REST reference
Every endpoint, grouped
All paths are relative to https://api.gocushy.com/api/v1. All require the bearer key.
Account & Stripe
/meAccount snapshot: id, name, email, plan, stripe_connected./stripe/connectStart (or resume) Stripe onboarding. Returns onboarding_url — the one step a human must complete./stripe/statusReturns stripe_account_id, charges_enabled, onboarded.PayPal & Airwallex
/paypal/connectConnect the merchant's own PayPal Business REST app: client_id, secret, environment (live | sandbox). Validated live before storing — encrypted. Paid plan only./paypal/statusReturns connected and environment./airwallex/connectConnect the merchant's own Airwallex account: client_id, api_key, environment (live | demo), optional webhook_secret for instant settlement. Reconnecting without webhook_secret keeps the saved one. Validated live before storing — encrypted. Paid plan only./airwallex/statusReturns connected and environment.Products
/productsNon-archived products, newest first./productsCreate a product. price_cents 50–99,999,999. type one_time (default) or subscription; subscriptions require interval month|year. Add trial_days (1–365) for a free trial.trial_period_days), Airwallex verifies a NZ$0 mandate and charges it at trial end, and PayPal uses a free trial billing cycle on the plan. The crypto rail sits trials out — a single on-chain payment can't promise a future charge. Nothing is charged on day one on any rail, and the first-payment date is disclosed on the checkout.A trialing order is deliberately not
paid and carries total_cents: 0 — no money has moved, and every revenue figure in gocushy filters on paid, so trials can never inflate your reported revenue. The real money shows up as a renewal order when the trial converts. Provision access on order.trial_started, not order.paid./import/stripe-productsImport the merchant's existing Stripe catalog. Idempotent — re-running updates instead of duplicating. Products without a usable default price, and recurring prices, are skipped and reported.curl -X POST https://api.gocushy.com/api/v1/products \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Launch Course",
"description": "The complete 6-module launch system.",
"price_cents": 4900,
"currency": "usd",
"delivery_url": "https://members.example.com/launch-course",
"delivery_note": "Log in with the email you purchased with."
}'{
"product": {
"id": "01J2QK5W8XN4V6T9R3E7M1B0AA",
"name": "Launch Course",
"description": "The complete 6-module launch system.",
"price_cents": 4900,
"currency": "usd",
"type": "one_time",
"interval": null,
"delivery_url": "https://members.example.com/launch-course",
"delivery_note": "Log in with the email you purchased with.",
"created_at": "2026-08-01T09:30:00.000000Z"
}
}Product parameters
| Field | Type | Rules |
|---|---|---|
name | string | required, ≤255 |
description | string | ≤5000 |
price_cents | integer | required, 50–99,999,999 |
trial_days | integer | optional, 1–365 · subscriptions only · card, PayPal and Airwallex (not crypto) |
currency | string | 3-letter ISO code, e.g. usd, nzd |
type | string | one_time (default) | subscription |
interval | string | month | year — required when type is subscription |
delivery_url | url | ≤2000 — becomes the "Access your purchase" button on receipts and the signed thank-you page (paid orders only) |
delivery_note | string | ≤500 — instructions shown with the access button |
Offers
/offersAll offers with products, checkout URLs, and blocks./offersCompose a checkout. Goes live immediately; slug auto-generated from the name./offers/{id}Update name, status (draft|live|archived), success_url, commission_bps, and checkout blocks./offers/{id}/pauseStop the offer taking orders (status draft) — the checkout link and short link stop working immediately. Content, stats and order history are kept. Idempotent; archived offers are refused./offers/{id}/resumeRe-open a paused offer. Refuses with 422 delivery_required if the offer's product has no delivery link or access note (and the account has no order.paid webhook) — unlike PUT status, which only warns. Returns warnings for undeliverable bumps and funnel steps./offers/{id}Delete (archive) an offer — its checkout link 404s immediately, order history is kept, coupons scoped to it are retired. Restore with PUT status.curl -X POST https://api.gocushy.com/api/v1/offers \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Launch Course",
"product_id": "01J2QK5W8XN4V6T9R3E7M1B0AA",
"bump_product_id": "01J2QK5W8XN4V6T9R3E7M1B0BB",
"bump_headline": "Add the workbook — printable worksheets for every module",
"upsell_product_id": "01J2QK5W8XN4V6T9R3E7M1B0CC",
"upsell_headline": "Upgrade: three live group coaching calls",
"commission_bps": 3000,
"checkout": {
"headline": "Launch your course in 30 days",
"subheadline": "The exact system, module by module — no theory.",
"bullets": [
"Six modules, one launch plan",
"Swipe files for every email",
"Lifetime updates included"
],
"testimonials": [
{
"quote": "Paid for itself in the first week.",
"name": "Mia T.",
"role": "Course creator"
}
],
"guarantee": {
"title": "30-day money-back guarantee",
"body": "Email us within 30 days for a full refund — no forms, no calls."
},
"deadline": "2026-08-15T23:59:00Z",
"button_text": "Get instant access",
"accent": "#4f46e5"
}
}'{
"offer": {
"id": "01J2QN7B2XN4V6T9R3E7M1B0DD",
"name": "Launch Course",
"slug": "launch-course",
"status": "live",
"checkout_url": "https://gocushy.com/c/01J2QJ3M8XN4V6T9R3E7M1B0CD/launch-course",
"cushy_link": "https://cushy.link/k3x9f2",
"product": { "id": "01J2QK5W8XN4V6T9R3E7M1B0AA", "name": "Launch Course", "price_cents": 4900 },
"bump_product": { "id": "01J2QK5W8XN4V6T9R3E7M1B0BB", "name": "Launch Workbook", "price_cents": 1700 },
"bump_headline": "Add the workbook — printable worksheets for every module",
"upsell_product": { "id": "01J2QK5W8XN4V6T9R3E7M1B0CC", "name": "Group Coaching", "price_cents": 9900 },
"upsell_headline": "Upgrade: three live group coaching calls",
"checkout_blocks": { "headline": "Launch your course in 30 days", "…": "…" }
}
}Offer parameters
| Field | Type | Rules |
|---|---|---|
name | string | required, ≤255 — the slug is generated from it |
product_id | id | required, must be your product |
bump_product_id | id | optional, must differ from the main product; requires bump_headline (≤255) |
upsell_product_id | id | optional, must differ from the main product; requires upsell_headline (≤255); upsell_body ≤5000 |
success_url | url | https only, ≤2000 — where the buyer lands after everything (post-upsell). Default: the hosted thank-you page. When set, the buyer is redirected here once the order is paid; we append order_id, total_cents & currency so your page can fire its own conversion pixels. Delivery links + receipt still go out by email. The founding-account claim step is never skipped. |
commission_bps | integer | 0–9000 — per-offer affiliate rate in basis points (3000 = 30%) |
checkout | object | null | See Checkout blocks |
422 invalid_offer if you try.Orders & refunds
/orders?limit=50Recent orders, newest first. limit defaults to 50./orders/export?from&toAccounting-grade CSV. Optional from/to dates (YYYY-MM-DD, inclusive)./orders/{id}/refundRefund a paid order — full by default, partial via amount_cents./orders/{id}/cancel-subscriptionCancel at period end (default) or pass immediately: true.curl "https://api.gocushy.com/api/v1/orders?limit=2" \ -H "Authorization: Bearer gc_your_key"
{
"orders": [
{
"id": "01J2QP8XN4V6T9R3E7M1B0CDEE",
"kind": "purchase",
"status": "paid",
"buyer_email": "buyer@example.com",
"buyer_country": "NZ",
"currency": "usd",
"subtotal_cents": 6600,
"tax_cents": 990,
"total_cents": 7590,
"refunded_cents": 0,
"bump_taken": true,
"upsell_taken": false,
"invoice_number": "ACME-0042",
"offer": { "id": "01J2QN7B2XN4V6T9R3E7M1B0DD", "name": "Launch Course", "slug": "launch-course" },
"created_at": "2026-08-01T10:13:58.000000Z"
}
]
}Order status: pending, paid, failed, refunded, partially_refunded. Order kind: purchase or renewal (subscription renewals auto-generate orders, invoices, and receipts).
curl -X POST https://api.gocushy.com/api/v1/orders/01J2QP8XN4V6T9R3E7M1B0CDEE/refund \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{ "amount_cents": 1700 }'{
"order": {
"id": "01J2QP8XN4V6T9R3E7M1B0CDEE",
"status": "partially_refunded",
"refunded_cents": 1700,
"total_cents": 7590
}
}Refunds cascade automatically: the order.refunded webhook fires, refund follow-up rules run in your ESP, and affiliate commissions are clawed back. Omit amount_cents to refund the full remaining balance. Only paid or partially-refunded orders are refundable (422 not_refundable otherwise), and you can never refund more than remains (amount_cents is capped server-side).
Subscriptions
Subscriptions are products with type: "subscription" and an interval, sold through single-product offers, billed on the merchant's own rail — Stripe or PayPal bill on their side; Airwallex renewals are charged by GoCushy's own scheduler against the saved mandate. Each renewal creates a new order with kind: "renewal" — it shows up in GET /orders, the CSV export, stats, webhooks, and Xero like any other paid order.
curl -X POST https://api.gocushy.com/api/v1/orders/01J2QP8XN4V6T9R3E7M1B0CDEE/cancel-subscription \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{}'
# → { "canceled": true, "when": "at period end" }
# Pass { "immediately": true } to cut access now.Orders without a subscription return 422 not_a_subscription.
Stats & export
/stats?days=30Rolling window, 1–365 days. Revenue, counts, take rates, per-offer breakdown./activity?limit=50The account's activity trail — actions by the merchant (actor: user), by agents (actor: ai, tagged with the API key), and by GoCushy itself (actor: system — orders paid/failed/refunded). Filters: actor, action prefix.{
"window_days": 30,
"revenue_cents": 412300,
"orders_paid": 61,
"orders_failed": 4,
"orders_refunded": 2,
"bump_take_rate": 0.377,
"upsell_take_rate": 0.213,
"by_offer": [
{ "name": "Launch Course", "orders": 48, "revenue_cents": 322000 },
{ "name": "Workbook Solo", "orders": 13, "revenue_cents": 90300 }
]
}The CSV export (GET /orders/export) returns these columns: date, invoice_number, order_id, kind, status, offer, buyer_email, buyer_country, currency, subtotal_cents, tax_cents, total_cents, bump, upsell, reverse_charged. Values are formula-escaped, so the file is safe to open directly in Excel or Sheets.
Connections & follow-ups
/connectionsConnected email providers with status and last error./connectionsConnect an email provider: mailchimp, activecampaign, kit, getresponse, drip, mailerlite, klaviyo, brevo, beehiiv, loops, flodesk, hubspot, or emailoctopus. Credentials are validated live (your audiences are fetched) before anything is stored — encrypted. Only ActiveCampaign also needs api_url./followupsAll follow-up rules across offers, with per-rule health: enabled, last_fired_at, connection status and last error./followupsMap an offer to an ESP action: trigger (purchase | refund | upsell_taken | renewal | abandoned | payment_failed | dunning_recovered), list_id, tags (≤20, each ≤100 chars). payment_failed fires once per dunning episode (declined one-time payment, failed renewal, or PayPal suspension — not per retry; PayPal fires the same suspension event for manual pauses); dunning_recovered fires when a failed subscription collects again. Checkout declines (money never moved) get the same EU/UK consent gate as abandoned and only fire while the offer is live. Tags-only rules are rejected on providers with no tag mechanic (Brevo, Flodesk, HubSpot) — use list_id there./followups/{id}Edit a rule (trigger, connection_id, list_id, tags) or pause it without losing its configuration: enabled: false./followups/{id}Remove a rule.abandoned trigger fires ~30–45 min after a buyer starts a payment (clicks pay or opens a payment window) without finishing — not merely typing an email and leaving. It's suppressed if they later buy from you. Your ESP sends the actual win-back emails: gocushy tags/lists the buyer into your connected provider and your ESP automation does the rest. EU/UK buyers reach your ESP only if they ticked the marketing-consent box gocushy shows automatically on checkouts with an enabled abandoned rule — you are the sender of record, so your ESP's unsubscribe applies.Business
/businessThe merchant business profile./businessUpdate business_name, business_address, tax_number, tax_country (ISO-2), invoice_prefix, support_email, collect_tax. Feeds the checkout footer, receipts, invoices, and Xero.collect_tax: true to charge real tax at checkout via Stripe Tax on the merchant's own registrations — including EU B2B reverse charge and NZ/AU GST. Requires Stripe Tax to be activated with the merchant's tax registrations on their connected account — set up through gocushy (Express connected accounts have no separate Stripe tax dashboard).Webhooks
/webhooksSubscribed endpoints with last_success_at and last_error./webhooksSubscribe an https endpoint. Returns the signing secret exactly once./webhooks/{id}Remove an endpoint.Full delivery and signature details in Webhooks & signatures below.
Verify purchase
/verify-purchase?email&product_id&offer_id"Did this email buy this?" — for gating access. Details in Verify purchase below.Affiliates & commissions
/affiliatesAll affiliates with codes and due balances (per currency)./affiliatesRegister an affiliate. Code auto-generated if omitted./affiliates/{id}/link?offer=Referral link for an offer (by ID or slug): the checkout URL + ?via=code. Attribution is server-side — no JavaScript fragility./affiliates/{id}/mark-paidSettle the released balance after you've actually paid them./commissions?affiliate_id&statusThe commission ledger. status: pending | due | paid | reversed. Returns up to 200 rows.curl -X POST https://api.gocushy.com/api/v1/affiliates \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Jess Parker",
"email": "jess@example.com",
"code": "jess",
"default_commission_bps": 3000,
"payout_note": "PayPal: jess@example.com"
}'{
"affiliate": {
"id": "01J2QR9C4XN4V6T9R3E7M1B0FF",
"name": "Jess Parker",
"email": "jess@example.com",
"code": "jess",
"default_commission_bps": 3000,
"payout_note": "PayPal: jess@example.com"
}
}How the ledger works: commissions accrue on referred sales at the rate locked at purchase, hold for a 30-day refund window as pending, then become due. Refunds create clawbacks that net against future releases. Recurring commissions accrue on renewals. Self-referrals are blocked.
Which rate applies (changed 3 Aug 2026): an affiliate's own default_commission_bps now wins over the offer's commission_bps, not the other way round. A rate set on an affiliate is a negotiated rate and must beat the house rate — previously a 40% offer rate silently overrode a partner signed at 50%, while their portal and welcome email both quoted 50%. An affiliate left at 0 is treated as "not negotiated" and falls through to the offer's rate. The rate is still frozen onto the order at purchase, so changing either number never re-prices a sale that has already happened.
curl -X POST https://api.gocushy.com/api/v1/affiliates/01J2QR9C4XN4V6T9R3E7M1B0FF/mark-paid \ -H "Authorization: Bearer gc_your_key"
{
"marked_paid_cents": 14700,
"marked_paid_by_currency": { "usd": 14700 },
"affiliate": { "id": "01J2QR9C4XN4V6T9R3E7M1B0FF", "name": "Jess Parker", "code": "jess", "payout_note": "PayPal: jess@example.com" },
"note": "Record the actual transfer with your payout provider — gocushy tracks the ledger, your money does the paying."
}Mark-paid settles per currency, and only when the net balance is positive — clawback debt is never "paid away"; it stays due and nets against future commissions.
04 · Checkout blocks
Described, not designed
You (or your agent) describe the selling content; GoCushy renders it into one battle-tested checkout layout. Every field is plain text, length-capped, and HTML-escaped into fixed zones — an agent can fully customise a checkout and structurally cannot break it. No templates, no custom CSS, no layout options. Set blocks via POST /offers, PUT /offers/{id}, or the update_checkout MCP tool.
| Field | Type | Limits & validation |
|---|---|---|
headline | string | ≤120 chars. Replaces the product name as the H1 (the product name moves to the order summary). Write the outcome, not the product name. |
subheadline | string | ≤200 chars. |
bullets | array of strings | ≤6 items, each ≤120 chars. Rendered as a checkmark list. Benefits, not features. |
testimonials | array of objects | ≤3 items: quote (required, ≤300), name (required, ≤80), role (optional, ≤80). Real testimonials only — fabrication violates the acceptable use policy. |
guarantee | object | title (required with guarantee, ≤80), body (optional, ≤240). Rendered as a badge box. |
deadline | ISO datetime | Must be in the future when set. Renders an honest countdown that hides once passed and can never reset — evergreen/fake timers are impossible by construction. |
image_url | url | https only, ≤2000 chars. Rendered as a plain image above the headline. |
button_text | string | ≤40 chars. Overrides the pay button label. |
legal_note | string | ≤500 chars, plain text (escaped). Legal small print under the pay button — e.g. an EU/UK right-of-withdrawal disclosure — repeated automatically in the buyer's receipt email as the durable-medium confirmation. Also settable account-wide via seller_defaults. |
accent | hex colour | Strict #rrggbb. Rejected if luminance (0.299R + 0.587G + 0.114B) / 255 exceeds 0.7 — button text is white and must stay readable. |
checkout object replaces the stored blocks entirely. Always send every field you want to keep. Send "checkout": null to reset to the plain checkout. Unknown keys are stripped server-side — only the nine fields above are stored.The untouchable core — payment form, order bump, totals, pay button — renders between your blocks and is computed server-side. An agent cannot ship a mispriced or broken checkout.
05 · Webhooks
Signed events, delivered with retries
Events
| Event | Fires when |
|---|---|
order.paid | An order is paid — including subscription renewals. |
order.refunded | An order is refunded, fully or partially. |
order.failed | A payment attempt fails. |
order.upsell_taken | The buyer accepts the one-click post-purchase upsell. |
order.trial_started | A free trial begins. No money has moved — the order is trialing with a trial_ends_at and a total of 0. Grant access on this event; order.paid follows when the trial converts. |
subscription.payment_failed | A renewal charge fails. The subscription is still live and will be retried — use this to warn the customer or pause access on your side. Not the same as order.failed, which is a first payment that never succeeded. |
order.paid alone will strand your trial customers. A trial grants access before any money moves, so the only event that fires is order.trial_started — a provisioning system listening for order.paid never hears about them, and the customer is left outside whatever they signed up for until the trial converts. If you sell trials, subscribe to both. Omit events entirely and you get a sensible default that already includes it.Subscribe
curl -X POST https://api.gocushy.com/api/v1/webhooks \
-H "Authorization: Bearer gc_your_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/gocushy", "events": ["order.paid", "order.trial_started", "order.refunded"] }'Leave events out and the webhook subscribes to order.paid, order.trial_started, order.refunded, order.failed and order.upsell_taken — everything that affects whether a customer should have access. subscription.payment_failed is opt-in, because whether a failed renewal should cut access is your policy, not ours.
{
"webhook": {
"id": "01J2QS0D5XN4V6T9R3E7M1B0GG",
"url": "https://example.com/webhooks/gocushy",
"events": ["order.paid", "order.trial_started", "order.refunded"],
"active": true
},
"signing_secret": "cwh_kJ8mN2pQ4rS6tU8vW0xY2zA4bC6dE8fG1hJ3kL5m",
"note": "Store this secret now — it is not shown again. Verify: hex HMAC-SHA256 of the raw request body, header X-Gocushy-Signature."
}The endpoint must be https. Omit events to subscribe to all four. The signing_secret (prefix cwh_) is returned exactly once — store it immediately.
Delivery
- Request:
POSTwith a JSON body and headersContent-Type: application/json,X-Gocushy-Event(the event name), andX-Gocushy-Signature. - Success: any 2xx response within 15 seconds.
- Retries: up to 5 attempts with backoff — 30s, 2 min, 10 min, then 60 min after the failed attempts.
- Health:
GET /webhooksshowslast_success_atand thelast_errorper endpoint.
{
"event": "order.paid",
"sent_at": "2026-08-01T10:14:07+00:00",
"order": {
"id": "01J2QP8XN4V6T9R3E7M1B0CDEE",
"status": "paid",
"buyer_email": "buyer@example.com",
"buyer_name": "Alex Buyer",
"total_cents": 7590,
"tax_cents": 990,
"currency": "usd",
"bump_taken": true,
"upsell_taken": false,
"metadata": { "igloo_group": "founders" },
"offer": { "id": "01J2QN7B2XN4V6T9R3E7M1B0DD", "name": "Launch Course", "slug": "launch-course" },
"items": [
{ "id": "01J2QR1A0XN4V6T9R3E7M1B0F1", "kind": "main", "product_id": "01J2QK5W8XN4V6T9R3E7M1B0AA", "product_name": "Launch Course", "amount_cents": 4900, "qty": 1, "unit_price_cents": null, "discount_cents": 0, "tax_cents": 0, "line_total_cents": null },
{ "id": "01J2QR1A0XN4V6T9R3E7M1B0F2", "kind": "bump", "product_id": "01J2QK5W8XN4V6T9R3E7M1B0BB", "product_name": "Launch Workbook", "amount_cents": 1700, "qty": 1, "unit_price_cents": 1700, "discount_cents": 0, "tax_cents": 0, "line_total_cents": 1700 }
],
"created_at": "2026-08-01T10:13:58+00:00"
}
}Custom metadata. Append ?meta[key]=value to any checkout link and GoCushy carries those pairs onto the order and echoes them back in order.metadata — use it to route provisioning (e.g. which plan or community to grant). Up to 20 string pairs; card-charging totals are never affected by metadata.
Item kind is main, bump, or upsell. Because retries exist, treat deliveries as at-least-once: key your processing on order.id + event to stay idempotent.
Parse permissively. Payloads gain keys over time and every addition is backwards-compatible — never reject a delivery for carrying a field you don't recognise, and don't validate against a closed schema. The per-item money split (qty, unit_price_cents, discount_cents, tax_cents, line_total_cents) is the current example: unit_price_cents and line_total_cents are null on rows written before the split existed, so read amount_cents as the fallback rather than assuming a number is there.
Verify the signature
X-Gocushy-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with your signing secret. Compute it over the exact bytes you received — parse the JSON only after verifying.
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post(
"/webhooks/gocushy",
express.raw({ type: "application/json" }), // raw body — do not use express.json() here
(req, res) => {
const signature = req.get("X-Gocushy-Signature") || "";
const expected = crypto
.createHmac("sha256", process.env.CUSHY_WEBHOOK_SECRET)
.update(req.body) // Buffer of the raw bytes
.digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(401).send("bad signature");
const { event, order } = JSON.parse(req.body);
if (event === "order.paid") {
// grant access for order.buyer_email
}
res.sendStatus(200);
}
);<?php
$secret = getenv('CUSHY_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CUSHY_SIGNATURE'] ?? '';
if (!hash_equals(hash_hmac('sha256', $raw, $secret), $signature)) {
http_response_code(401);
exit('bad signature');
}
$payload = json_decode($raw, true);
if ($payload['event'] === 'order.paid') {
// grant access for $payload['order']['buyer_email']
}
http_response_code(200);06 · Verify purchase
Gate anything on a real purchase
One GET answers "did this email buy this?" — use it to gate course access, community invites, downloads, or support. Paid and partially-refunded orders count; fully refunded ones don't.
| Param | Type | Notes |
|---|---|---|
email | string | required — matched case-insensitively |
product_id | id | optional — only orders containing this product (main, bump, or upsell) |
offer_id | id | optional — only orders through this offer |
curl "https://api.gocushy.com/api/v1/verify-purchase?email=buyer%40example.com&product_id=01J2QK5W8XN4V6T9R3E7M1B0AA" \ -H "Authorization: Bearer gc_your_key"
{
"purchased": true,
"orders": [
{
"id": "01J2QP8XN4V6T9R3E7M1B0CDEE",
"offer_id": "01J2QN7B2XN4V6T9R3E7M1B0DD",
"status": "paid",
"total_cents": 7590,
"currency": "usd",
"created_at": "2026-08-01T10:13:58.000000Z"
}
]
}Example: gate an Express route
async function requirePurchase(req, res, next) {
const email = req.user.email; // however you identify the visitor
const params = new URLSearchParams({
email,
product_id: process.env.COURSE_PRODUCT_ID,
});
const r = await fetch(
`https://api.gocushy.com/api/v1/verify-purchase?${params}`,
{ headers: { Authorization: `Bearer ${process.env.GOCUSHY_API_KEY}` } }
);
const { purchased } = await r.json();
if (!purchased) return res.status(403).send("No purchase found for this email.");
next();
}
app.get("/course", requirePurchase, (req, res) => res.render("course"));07 · Put the checkout anywhere
Three ways to include a checkout
Link to it, pop it over your page, or embed it inline. All three settle on the merchant's own Stripe. Works inside anything that renders HTML — Lovable output, WordPress, Webflow, hand-coded pages. The script has no dependencies, ~4 KB.
1. Link — simplest, no script
Send the buyer to the hosted checkout. Works everywhere, including email.
<a href="https://gocushy.com/c/YOUR_ACCOUNT_ID/your-offer-slug">Buy now</a>
2. Lightbox — a popup over your page
Any element with data-cushy opens the checkout in a centered overlay (iframe with allow="payment"). Escape or a click outside closes it. The href is the checkout URL; set data-cushy-url to keep a different href.
<a href="https://gocushy.com/c/YOUR_ACCOUNT_ID/your-offer-slug" data-cushy>Buy now</a> <script src="https://api.gocushy.com/embed.js" defer></script>
3. Inline — the checkout inside your page
A container with data-cushy-inline renders the checkout right where it sits (great for a dedicated /checkout page). Give it room — it fills the container.
<div data-cushy-inline="https://gocushy.com/c/YOUR_ACCOUNT_ID/your-offer-slug"></div> <script src="https://api.gocushy.com/embed.js" defer></script>
- Progressive: without JavaScript the link still works — it just navigates to the hosted checkout. The script upgrades it.
- SPA-safe: a MutationObserver re-binds elements injected after load — builders and client-side routers included.
- Programmatic:
window.Cushy.open(url),window.Cushy.inline(el, url),window.Cushy.close().
Your agent can fetch all three snippets pre-filled for any offer with the get_embed_code MCP tool (pass style = link, lightbox, inline, or all).
08 · Going live
Test vs live is just which Stripe you connect
There is no separate sandbox account in GoCushy. The mode is decided by which Stripe account the merchant connects — connect a Stripe test-mode account to develop with test cards; connect the real account and the same offers, webhooks, and tools sell for real. The platform handles both identically.
Launch checklist
- Connect the real Stripe —
POST /stripe/connect, complete onboarding, confirm withGET /stripe/status(charges_enabled: true). - Set business details — name, address, tax registration, invoice prefix. This is what appears on checkouts, receipts, and compliant invoices.
- Wire your systems — a webhook for
order.paid, follow-up rules for your ESP,verify-purchasefor gating. - Point real traffic at the hosted checkout link, the embed, or affiliate links.
For agents
Machine-readable platform discovery lives at api.gocushy.com/llms.txt — what an agent can do here, how to connect, and the guardrails it can rely on: server-side pricing, at-most-once upsell charges, signed buyer-sensitive URLs, honest countdowns by construction.