Subscription checkout
Single endpoint, two paths.
Endpoint
POST /agents/:agentId/pricing/:tierId/checkout
Body: { "returnUrl": "https://app.my-agent.com/post-checkout" }
Free tier path
Paid tier path
Return URL validation
The backend rejects any returnUrl that doesn't match one of:
- An entry in the agent's
redirectUris[]. - The agent's
productDomain(origin match). - (Dev only) any
http://localhost:*origin.
Mismatch → 422 UNPROCESSABLE_ENTITY with error.code === 'INVALID_RETURN_URL'. This is the marketplace's primary defense against open redirects via the checkout flow.
Race: did the webhook arrive yet?
After Stripe redirects the user back to returnUrl, there is a small window where the webhook hasn't been processed. Recommended UX:
- On the post-checkout page, call
GET /me/subscriptions/:agentIdonce. - If 404 / not active, show "Finalizing your subscription…" and poll every 1.5s up to 15s.
- Then refresh.
The webhook usually lands in < 1s, but TLS + signature verification + DB write can occasionally exceed 3s.
Existing subscription handling
If the user already has an active or trialing subscription on the same agent:
- Free → paid: backend creates a Stripe Checkout that includes a
proration_behavior: 'create_prorations'if you wire it that way (current default: just create a new sub; the webhook update merges it via partial unique index). - Paid → paid (upgrade): goes through Stripe's checkout but Stripe handles the proration on its side.
- Paid → free: not currently supported via checkout; user must cancel first.
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | UNAUTHORIZED | Not signed in |
| 404 | NOT_FOUND | Agent or tier doesn't exist |
| 422 | INVALID_RETURN_URL | returnUrl not in allow-list |
| 503 | SERVICE_UNAVAILABLE | Stripe down or STRIPE_SECRET_KEY missing |