{
  "name": "app-payments",
  "title": "App API - Payments (Stripe)",
  "description": "Charge your app's end-users via Stripe: one-time purchases + subscriptions through Stripe Connect. The owner connects their own Stripe account; Gipity brokers checkout and verified webhooks and takes a small platform fee.",
  "guid": "sk_plat_apay",
  "category": "App services",
  "requiredTools": [
    "payment_setup"
  ],
  "content": "# App API - Payments (Stripe)\n\nLet a deployed app charge **its own end-users** for one-time purchases and subscriptions. Built on **Stripe Connect**: the app owner connects *their own* Stripe account, money lands there, and Gipity takes a small platform fee (default 1%) via a Stripe `application_fee`. The owner is the merchant of record (they bear Stripe's processing fees and disputes).\n\nThe fastest path is the **`stripe` kit** — it ships the buy button, the subscription-status gate, the fulfillment function, and the database tables. Add it to a database-backed app (`web-fullstack` or `api`), or start from the **`paid-app`** starter, which has it pre-installed.\n\n> Why the platform is involved: an app's serverless functions can't safely verify Stripe webhooks (no raw body, no crypto in the sandbox). So the platform calls Stripe with the owner's connected account and verifies every webhook, then forwards the trusted event to the kit's `payment-events` function. You don't wire any of that up.\n\n## Setup (owner, once)\n\n```bash\ngipity add stripe            # if not already installed (the paid-app starter has it)\ngipity payments connect      # opens Stripe-hosted onboarding (bank + identity; NO API keys to paste)\ngipity payments status       # confirm \"charges enabled\"\ngipity deploy dev\n```\n\nThe agent equivalent is the `payment_setup` tool (`action: 'connect' | 'status'`).\n\n## Use the kit (frontend)\n\n```js\nimport { checkout, getSubscriptionStatus, openBillingPortal } from '@gipity/stripe';\n\n// One-time purchase. Amounts are in the currency's smallest unit (cents).\ncheckout({ items: [{ name: 'Lifetime access', amount: 4900 }] });\n\n// Monthly subscription (interval is required for subscription mode).\ncheckout({ mode: 'subscription', items: [{ name: 'Pro', amount: 900, interval: 'month' }] });\n\n// Gate members-only UI.\nconst { active } = await getSubscriptionStatus();\nif (active) showProFeatures();\n\n// Let a subscriber manage / cancel billing (Stripe Customer Portal).\nopenBillingPortal();\n```\n\nOr the zero-JS element:\n\n```html\n<stripe-buy-button name=\"Lifetime access\" amount=\"4900\">Buy</stripe-buy-button>\n<stripe-buy-button name=\"Pro\" amount=\"900\" mode=\"subscription\" interval=\"month\">Subscribe</stripe-buy-button>\n<stripe-buy-button price-id=\"price_123\" mode=\"subscription\">Subscribe</stripe-buy-button>\n```\n\n`checkout()` auto-attaches the signed-in Gipity user, so purchases and subscriptions are attributed to them — which is exactly what `getSubscriptionStatus()` reads. Encourage users to sign in before subscribing.\n\n## Fulfillment\n\nWhen a payment completes, the platform calls the kit's **`payment-events`** function (public, platform-invoked) with a signature-verified event and it records to two tables:\n\n- **`payments`** — one row per completed checkout (idempotent on `stripe_session_id`).\n- **`subscriptions`** — one row per Stripe subscription, upserted across its lifecycle (active → past_due → canceled).\n\n`payment-events` is kit-owned (sealed). Put your *own* fulfillment (grant a role, send a receipt) in your app code or a workflow, triggered off new rows in those tables — don't edit the sealed function.\n\n## The service endpoints (under the hood)\n\nThe kit calls these for you; reach for them directly only for a custom flow. All are under `https://a.gipity.ai/api/<PROJECT_GUID>/services/payments/` and use the same `X-App-Token` auth as the other app services (from the browser, `Gipity.service('payments/checkout', body)` attaches it automatically).\n\n| Endpoint | Who | Body | Returns |\n|---|---|---|---|\n| `POST /payments/connect` | owner | `{ returnUrl? }` | `{ url, connectedAccountId, status }` |\n| `GET  /payments/status` | owner | — | `{ connected, charges_enabled, payouts_enabled, details_submitted }` |\n| `POST /payments/checkout` | end-user | `{ mode, items, successUrl, cancelUrl, clientReferenceId?, customerEmail?, metadata? }` | `{ checkoutUrl, sessionId }` |\n| `POST /payments/portal` | end-user | `{ customerId, returnUrl }` | `{ portalUrl }` |\n\n`items` accept either inline `{ name, amount, currency?, interval?, quantity? }` (no pre-created Stripe products needed) or a `{ priceId }`. The platform fee is applied automatically (`application_fee_amount` for one-time, `application_fee_percent` for subscriptions).\n\n## Common mistakes to avoid\n\n- **Calling checkout before onboarding is done** returns 409 (`CONFLICT`) — run `gipity payments connect` and wait for `charges_enabled: true`.\n- **Amounts are in cents**, not dollars: `$9.00` is `900`.\n- **Subscription items need an `interval`** (`day`/`week`/`month`/`year`); omitting it errors.\n- **Don't try to verify Stripe webhooks inside an app function** — the platform already does it and calls `payment-events` with a trusted event. Don't expose your own raw webhook.\n- **Test in Stripe test mode** with card `4242 4242 4242 4242`.\n\n## Related skills\n\n- [app-development](app-development.md) — writing functions, `gipity.yaml`, permissions\n- [deploy](deploy.md) — deploy phases (files / database / functions)\n"
}
