← Back to the merchant guide Developer documentation

The sales machinery layer, fully scriptable.

Everything on this page is live in production: 52 MCP tools, a full REST API, HMAC-signed webhooks, purchase verification, and a 3 KB embed. Money always lands in the merchant's own accounts — their Stripe, PayPal, Airwallex, or wallet — GoCushy never holds funds.

01 · Overview

Base URL, auth, limits, errors

One base URL. One auth header. JSON in, JSON out.

base url
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.

bash
curl https://api.gocushy.com/api/v1/me \
  -H "Authorization: Bearer gc_your_key"
200 · json
{
  "account": {
    "id": "01J2QJ3M8XN4V6T9R3E7M1B0CD",
    "name": "Acme Courses",
    "email": "sam@example.com",
    "plan": "founding",
    "stripe_connected": true
  }
}

Conventions

  • Money is integer cents4900 = 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/json and Accept: 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.

StatusShapeWhen
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).
429Throttle responseOver 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.

bash
GOCUSHY_API_KEY=gc_your_key npx -y @gocushy/mcp

Config JSON for Claude Desktop or Cursor (claude_desktop_config.json / .cursor/mcp.json):

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 your gc_ 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 headerhttps://mcp.gocushy.com/mcp with Authorization: Bearer gc_your_key

Step by step in ChatGPT

If you can't find "Connectors" in ChatGPT, you're not missing anything — it is switched off until you turn on Developer mode, and it isn't where you'd expect. This is the single most common setup question we get, and none of it is a GoCushy setting.
  1. 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.
  2. Turn on Developer mode. Profile icon → SettingsApps & 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.)
  3. Create the connector. Back in Apps & Connectors a Create button now appears. Name it GoCushy and use this URL exactly, including /mcp and with no key in it:
    MCP server URL
    https://mcp.gocushy.com/mcp
  4. 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.
  5. 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.

Registry: the server is listed in the official MCP registry as com.gocushy/mcp. Agents can also discover the platform via api.gocushy.com/llms.txt.

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.

ToolWhat it doesKey inputs
connect_paymentStarts Stripe onboarding; returns the hosted URL the human must complete.
payment_statusChecks whether onboarding finished and charges are enabled.
create_productCreates 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_productUpdates 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_defaultsGets 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_productsImports active products from the connected Stripe account. Idempotent.
list_productsLists products with IDs and prices.
create_offerComposes 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_checkoutSets 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_offerStops 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_offerRe-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_linkHosted checkout URL (and short gocushy.com/l link) for an offer.offer (name, slug, or ID)
get_embed_codeCopy-paste snippets to include a checkout — link, lightbox popup, or inline embed.offer, style
list_ordersRecent orders: buyer, status, totals, bump/upsell taken.limit (1–100, default 20)
get_salesRevenue, order counts, bump/upsell take rates, per-offer breakdown.days (1–365, default 30)
get_offer_statsDeep-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_reportRevenue 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_behindThe 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_roasOne line per traffic source: revenue, rebills, LTV, and ROAS where ad spend has been reported. Trace subscription.days
set_conversion_trackingSends 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_linkMints 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_linkRe-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_linksEvery campaign short link with its tags, destination, and click count — busiest first.
report_ad_spendRecords 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_emailConnects 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_notificationsChat 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_paypalConnects 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_walletSets 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_followupsAll follow-up rules with per-rule health (enabled, last fired, connection errors). Call before set_followup to avoid duplicates.
set_followupMaps 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_followupEdits a rule, or pauses it without losing its configuration.followup_id, any of trigger, connection_id, list_id, tags, enabled
delete_followupRemoves a rule (stops future fan-out; already-tagged buyers stay in your ESP).followup_id
set_business_detailsSets 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_statusWhether Stripe Tax is active on the merchant's connected account and which registrations exist.
activate_taxActivates 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_registrationRegisters 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_registrationsThe merchant's Stripe Tax registrations (country + status).
create_webhookSubscribes an endpoint to order events. Returns the HMAC signing secret exactly once.url (https), events
verify_purchaseChecks whether an email bought from this merchant — for gating content and support.email, product_id, offer_id
create_couponCreates 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_couponsAll coupons with discount, scope, redemption counts, expiry, and active status.
deactivate_couponEnds 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_couponTurns a deactivated code back on, still subject to its original expiry and redemption cap. Admin key required.coupon_id
get_activityThe account's activity trail: merchant, agent, and system actions, newest first.limit, actor (user | ai | system), action
create_affiliateRegisters an affiliate; returns their referral code. Rates in basis points.name, email, code, default_commission_bps, payout_note
list_affiliatesAll affiliates with referral codes and currently-due balances.
get_affiliate_linkShareable referral link for an affiliate + offer. Attribution is server-side.affiliate_id, offer
mark_commissions_paidRecords that the merchant sent an affiliate their released balance. Bookkeeping, not a money movement.affiliate_id
cancel_subscriptionCancels a buyer's subscription — at period end by default, or immediately.order_id, immediately
refund_orderRefunds 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

GET/meAccount snapshot: id, name, email, plan, stripe_connected.
POST/stripe/connectStart (or resume) Stripe onboarding. Returns onboarding_url — the one step a human must complete.
GET/stripe/statusReturns stripe_account_id, charges_enabled, onboarded.

PayPal & Airwallex

POST/paypal/connectConnect the merchant's own PayPal Business REST app: client_id, secret, environment (live | sandbox). Validated live before storing — encrypted. Paid plan only.
GET/paypal/statusReturns connected and environment.
POST/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.
GET/airwallex/statusReturns connected and environment.

Products

GET/productsNon-archived products, newest first.
POST/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.
Free trials run on every subscription rail. Stripe vaults the card up front (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.
POST/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.
bash · create product
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."
  }'
201 · json
{
  "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

FieldTypeRules
namestringrequired, ≤255
descriptionstring≤5000
price_centsintegerrequired, 50–99,999,999
trial_daysintegeroptional, 1–365 · subscriptions only · card, PayPal and Airwallex (not crypto)
currencystring3-letter ISO code, e.g. usd, nzd
typestringone_time (default) | subscription
intervalstringmonth | year — required when type is subscription
delivery_urlurl≤2000 — becomes the "Access your purchase" button on receipts and the signed thank-you page (paid orders only)
delivery_notestring≤500 — instructions shown with the access button

Offers

GET/offersAll offers with products, checkout URLs, and blocks.
POST/offersCompose a checkout. Goes live immediately; slug auto-generated from the name.
PUT/offers/{id}Update name, status (draft|live|archived), success_url, commission_bps, and checkout blocks.
POST/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.
POST/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.
DELETE/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.
bash · create offer with checkout blocks
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"
    }
  }'
201 · json (abridged)
{
  "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

FieldTypeRules
namestringrequired, ≤255 — the slug is generated from it
product_ididrequired, must be your product
bump_product_ididoptional, must differ from the main product; requires bump_headline (≤255)
upsell_product_ididoptional, must differ from the main product; requires upsell_headline (≤255); upsell_body ≤5000
success_urlurlhttps 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_bpsinteger0–9000 — per-offer affiliate rate in basis points (3000 = 30%)
checkoutobject | nullSee Checkout blocks
Subscription rules (v1): subscription offers are single-product — no bumps or upsells on recurring offers, and subscription products can't be used as bumps or upsells on one-time offers. The API returns 422 invalid_offer if you try.

Orders & refunds

GET/orders?limit=50Recent orders, newest first. limit defaults to 50.
GET/orders/export?from&toAccounting-grade CSV. Optional from/to dates (YYYY-MM-DD, inclusive).
POST/orders/{id}/refundRefund a paid order — full by default, partial via amount_cents.
POST/orders/{id}/cancel-subscriptionCancel at period end (default) or pass immediately: true.
bash · list orders
curl "https://api.gocushy.com/api/v1/orders?limit=2" \
  -H "Authorization: Bearer gc_your_key"
200 · json
{
  "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).

bash · partial refund
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 }'
200 · json (abridged)
{
  "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.

bash · cancel at period end
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

GET/stats?days=30Rolling window, 1–365 days. Revenue, counts, take rates, per-offer breakdown.
GET/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.
200 · json
{
  "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

GET/connectionsConnected email providers with status and last error.
POST/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.
GET/followupsAll follow-up rules across offers, with per-rule health: enabled, last_fired_at, connection status and last error.
POST/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.
PATCH/followups/{id}Edit a rule (trigger, connection_id, list_id, tags) or pause it without losing its configuration: enabled: false.
DELETE/followups/{id}Remove a rule.
Abandoned checkouts, honestly: the 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

GET/businessThe merchant business profile.
PUT/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.
Tax: set 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

GET/webhooksSubscribed endpoints with last_success_at and last_error.
POST/webhooksSubscribe an https endpoint. Returns the signing secret exactly once.
DELETE/webhooks/{id}Remove an endpoint.

Full delivery and signature details in Webhooks & signatures below.

Verify purchase

GET/verify-purchase?email&product_id&offer_id"Did this email buy this?" — for gating access. Details in Verify purchase below.

Affiliates & commissions

GET/affiliatesAll affiliates with codes and due balances (per currency).
POST/affiliatesRegister an affiliate. Code auto-generated if omitted.
GET/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.
POST/affiliates/{id}/mark-paidSettle the released balance after you've actually paid them.
GET/commissions?affiliate_id&statusThe commission ledger. status: pending | due | paid | reversed. Returns up to 200 rows.
bash · create affiliate
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"
  }'
201 · json
{
  "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.

bash · mark commissions paid
curl -X POST https://api.gocushy.com/api/v1/affiliates/01J2QR9C4XN4V6T9R3E7M1B0FF/mark-paid \
  -H "Authorization: Bearer gc_your_key"
200 · json
{
  "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.

FieldTypeLimits & validation
headlinestring≤120 chars. Replaces the product name as the H1 (the product name moves to the order summary). Write the outcome, not the product name.
subheadlinestring≤200 chars.
bulletsarray of strings≤6 items, each ≤120 chars. Rendered as a checkmark list. Benefits, not features.
testimonialsarray of objects≤3 items: quote (required, ≤300), name (required, ≤80), role (optional, ≤80). Real testimonials only — fabrication violates the acceptable use policy.
guaranteeobjecttitle (required with guarantee, ≤80), body (optional, ≤240). Rendered as a badge box.
deadlineISO datetimeMust 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_urlurlhttps only, ≤2000 chars. Rendered as a plain image above the headline.
button_textstring≤40 chars. Overrides the pay button label.
legal_notestring≤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.
accenthex colourStrict #rrggbb. Rejected if luminance (0.299R + 0.587G + 0.114B) / 255 exceeds 0.7 — button text is white and must stay readable.
Replace semantics: on update, the 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

EventFires when
order.paidAn order is paid — including subscription renewals.
order.refundedAn order is refunded, fully or partially.
order.failedA payment attempt fails.
order.upsell_takenThe buyer accepts the one-click post-purchase upsell.
order.trial_startedA 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_failedA 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.
Subscribing to 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

bash · create webhook
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.

201 · json
{
  "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: POST with a JSON body and headers Content-Type: application/json, X-Gocushy-Event (the event name), and X-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 /webhooks shows last_success_at and the last_error per endpoint.
delivery payload · json
{
  "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.

node · express
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
<?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.

ParamTypeNotes
emailstringrequired — matched case-insensitively
product_ididoptional — only orders containing this product (main, bump, or upsell)
offer_ididoptional — only orders through this offer
bash
curl "https://api.gocushy.com/api/v1/verify-purchase?email=buyer%40example.com&product_id=01J2QK5W8XN4V6T9R3E7M1B0AA" \
  -H "Authorization: Bearer gc_your_key"
200 · json
{
  "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

node · middleware
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"));
Tip: call this from your server, never the browser — the API key is a merchant credential. Cache positive results; the 120 req/min limit is shared with the rest of your key's traffic.

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.

html
<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.

html
<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.

html
<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 StripePOST /stripe/connect, complete onboarding, confirm with GET /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-purchase for 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.

Questions? hello@gocushy.com — or point your agent at the MCP server and ask it.