Let 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).
The 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.
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-eventsfunction. You don't wire any of that up.
Setup (owner, once)
gipity add stripe # if not already installed (the paid-app starter has it)
gipity payments connect # opens Stripe-hosted onboarding (bank + identity; NO API keys to paste)
gipity payments status # confirm "charges enabled"
gipity deploy dev
The agent equivalent is the payment_setup tool (action: 'connect' | 'status').
Use the kit (frontend)
import { checkout, getSubscriptionStatus, openBillingPortal } from '@gipity/stripe';
// One-time purchase. Amounts are in the currency's smallest unit (cents).
checkout({ items: [{ name: 'Lifetime access', amount: 4900 }] });
// Monthly subscription (interval is required for subscription mode).
checkout({ mode: 'subscription', items: [{ name: 'Pro', amount: 900, interval: 'month' }] });
// Gate members-only UI.
const { active } = await getSubscriptionStatus();
if (active) showProFeatures();
// Let a subscriber manage / cancel billing (Stripe Customer Portal).
openBillingPortal();
Or the zero-JS element:
<stripe-buy-button name="Lifetime access" amount="4900">Buy</stripe-buy-button>
<stripe-buy-button name="Pro" amount="900" mode="subscription" interval="month">Subscribe</stripe-buy-button>
<stripe-buy-button price-id="price_123" mode="subscription">Subscribe</stripe-buy-button>
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.
Fulfillment
When 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:
payments— one row per completed checkout (idempotent onstripe_session_id).subscriptions— one row per Stripe subscription, upserted across its lifecycle (active → past_due → canceled).
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.
The service endpoints (under the hood)
The 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).
| Endpoint | Who | Body | Returns |
|---|---|---|---|
POST /payments/connect |
owner | { returnUrl? } |
{ url, connectedAccountId, status } |
GET /payments/status |
owner | — | { connected, charges_enabled, payouts_enabled, details_submitted } |
POST /payments/checkout |
end-user | { mode, items, successUrl, cancelUrl, clientReferenceId?, customerEmail?, metadata? } |
{ checkoutUrl, sessionId } |
POST /payments/portal |
end-user | { customerId, returnUrl } |
{ portalUrl } |
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).
Common mistakes to avoid
- Calling checkout before onboarding is done returns 409 (
CONFLICT) — rungipity payments connectand wait forcharges_enabled: true. - Amounts are in cents, not dollars:
$9.00is900. - Subscription items need an
interval(day/week/month/year); omitting it errors. - Don't try to verify Stripe webhooks inside an app function — the platform already does it and calls
payment-eventswith a trusted event. Don't expose your own raw webhook. - Test in Stripe test mode with card
4242 4242 4242 4242.
Related skills
- app-development — writing functions,
gipity.yaml, permissions - deploy — deploy phases (files / database / functions)