Developer documentation

The sales machinery layer, fully scriptable.

Everything on this page is live in production: 22 MCP tools, a full REST API, HMAC-signed webhooks, purchase verification, and a 3 KB embed. Money always lands in the merchant's own Stripe — 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 = $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 22 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. Two ways to authenticate:

  • Key in the path — for clients that can't send custom headers (e.g. ChatGPT developer mode): https://gocushy-mcp.fly.dev/mcp/gc_your_key
  • Bearer headerhttps://gocushy-mcp.fly.dev/mcp with Authorization: Bearer gc_your_key
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 22 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.name, price_cents, currency, type (one_time | subscription), interval (month | year), delivery_url, delivery_note
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. Replaces existing blocks.offer_id, checkout (object or null)
get_checkout_linkHosted checkout URL (and short cush.link) for an offer.offer (name, slug, or ID)
get_embed_codeCopy-paste snippet that overlays the checkout on any website.offer
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)
connect_emailConnects one of 13 providers: mailchimp, activecampaign, kit, getresponse, drip, mailerlite, klaviyo, brevo, beehiiv, loops, flodesk, hubspot, emailoctopus. Credentials validated live, stored encrypted; returns audiences.provider, api_key, api_url (ActiveCampaign), default_list_id
set_followupMaps an offer to email actions: list + tags on a trigger.offer_id, connection_id, trigger (purchase | refund | upsell_taken | renewal | abandoned), list_id, tags
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
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_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.

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.
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
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.
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": "#1a8a60"
    }
  }'
201 · json (abridged)
{
  "offer": {
    "id": "01J2QN7B2XN4V6T9R3E7M1B0DD",
    "name": "Launch Course",
    "slug": "launch-course",
    "status": "live",
    "checkout_url": "https://gocushy.com/c/01J2QJ3M8XN4V6T9R3E7M1B0CD/launch-course",
    "cush_link": "https://cush.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_urlurl≤2000 — where the buyer lands after everything (default: hosted thank-you page)
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 directly on the merchant's Stripe. 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.
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.
POST/followupsMap an offer to an ESP action: trigger (purchase | refund | upsell_taken | renewal | abandoned), list_id, tags (≤20, each ≤100 chars).

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 activated in the merchant's Stripe 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 (per-offer commission_bps wins over the affiliate default), 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.

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

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.refunded"] }'
201 · json
{
  "webhook": {
    "id": "01J2QS0D5XN4V6T9R3E7M1B0GG",
    "url": "https://example.com/webhooks/gocushy",
    "events": ["order.paid", "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-Cushy-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-Cushy-Event (the event name), and X-Cushy-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,
    "offer": { "id": "01J2QN7B2XN4V6T9R3E7M1B0DD", "name": "Launch Course", "slug": "launch-course" },
    "items": [
      { "kind": "main", "product_id": "01J2QK5W8XN4V6T9R3E7M1B0AA", "product_name": "Launch Course", "amount_cents": 4900 },
      { "kind": "bump", "product_id": "01J2QK5W8XN4V6T9R3E7M1B0BB", "product_name": "Launch Workbook", "amount_cents": 1700 }
    ],
    "created_at": "2026-08-01T10:13:58+00:00"
  }
}

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.

Verify the signature

X-Cushy-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-Cushy-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 · Embed

Checkout overlay on any site

One attribute, one script tag. Works inside anything that renders HTML — Lovable output, WordPress, Webflow, hand-coded pages. No dependencies, ~3 KB.

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>

Clicking 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 used as the checkout URL; set data-cushy-url instead to keep a different href.

  • Progressive: without JavaScript the link still works — it just navigates to the hosted checkout. The script upgrades it to an overlay.
  • SPA-safe: a MutationObserver re-binds buttons injected after page load — builders and client-side routers included.
  • Programmatic: window.Cushy.open(url) and window.Cushy.close().

Your agent can fetch this snippet pre-filled for any offer with the get_embed_code MCP tool.

08 · Going live

Test vs live is just which Stripe you connect

There is no sandbox toggle 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.