API reference

REST only, everything under /v1/* except provider webhooks and /health. Interactive Swagger lives at /api (non-production). This page is the orientation map; exact request/response shapes are in Swagger.

Authentication

Two principals, three guards:

CallerHowGuard behavior
End userAuthorization: Bearer <jwt> (issued by INITE Auth; RS256 via JWKS, HS256 fallback for dev; iss/aud/sub rules: Triad token contract)userId always comes from the token — endpoints ignore user IDs in params/body for non-service callers (IDOR guard)
INITE module (service)x-api-key: <Service.apiKey> (created in admin UI, revocable)May act on behalf of any user; serviceId is auto-injected from the key

JwtOrServiceGuard picks the principal from the headers: an x-api-key is resolved to an active Service, otherwise the Bearer JWT is verified against the JWKS. Either way, userId for user-scoped reads comes from the token — never from the request body.

# User
curl -H "Authorization: Bearer <jwt>" http://localhost:3000/v1/orders/me

# Service-to-service
curl -H "x-api-key: sk_..." http://localhost:3000/v1/credits/consume -d '...'

Admin endpoints additionally require the admin role in the JWT.

Surface map

Catalog & checkout

EndpointAuthNotes
GET /v1/productsoptional service keyActive products (service-scoped when key present)
GET /v1/products/prices?product_code=optional service keyPrices
GET /v1/products/search?q=optional service keySemantic search (pgvector; ILIKE fallback)
POST /v1/checkout/sessionsJWT or serviceCreate session; idempotency-key header supported
GET /v1/checkout/sessions/:idJWT or serviceSession details incl. payment methods
POST /v1/checkout/sessions/:id/payJWT or servicePay (promo applied inside this transaction)
POST /v1/checkout/validate-promoJWT or serviceDry-run promo validation

Orders, subscriptions, entitlements

EndpointAuth
GET /v1/orders/me, GET /v1/orders/:idJWT
GET /v1/subscriptions/me, POST /v1/subscriptions/cancel, POST /v1/subscriptions/resume, POST /v1/subscriptions/trialJWT
GET /v1/subscriptions/user/:userIdservice
GET /v1/entitlements/meJWT
GET /v1/entitlements/:userIdservice

Credits & metering

EndpointAuthNotes
GET /v1/credits/me, GET /v1/credits/me/usageJWTBalances, ledger history
GET /v1/credits/me/usage/breakdown?groupBy=feature|dayJWTMetered usage analytics
GET /v1/credits/featuresJWT or serviceActive feature registry (codes, units, rates)
POST /v1/credits/consumeJWT or serviceTwo modes: flat {userId, amount} (legacy, unchanged) or metered {userId, featureCode, units, modelTier?} — quota-enforced, returns creditsCharged; hard cap → 200 {success:false, error:"Quota exceeded", quota:{...}}
POST /v1/credits/adjustservice onlySigned adjustment
GET /v1/credits/:userIdserviceBalance lookup

Assistant & conversations

EndpointAuthNotes
POST /v1/assistant/chatJWTSSE — AI SDK v7 UI Message Stream (x-vercel-ai-ui-message-stream: v1); throttled 10/min
POST /v1/assistant/generate-featuresJWTOne-shot product-copy generation
GET /v1/assistant/actions?conversationId=JWTList action proposals
POST /v1/assistant/actions/:id/confirm / :id/rejectJWTExecute/decline a proposal; owner-only, role re-checked; double-confirm → 409
GET/POST /v1/conversations...JWT or serviceHistory CRUD (ownership-checked)
POST /v1/conversations/:id/messages/:messageId/feedbackJWT👍/👎 + optional comment on assistant messages

Notifications

EndpointAuth
GET /v1/notifications/me, GET /v1/notifications/me/unread-countJWT
POST /v1/notifications/me/read {ids?|all}JWT
GET/PUT /v1/notifications/me/preferencesJWT
POST /v1/notifications/unsubscribe {token, category?}public (email links)
POST /v1/notifications/webhooks/resendsvix signature

Recommendations

EndpointAuth
GET /v1/recommendations/me?surface=&limit=JWT
GET /v1/recommendations/checkout/:sessionIdJWT or service
GET /v1/recommendations/:userIdservice

Affiliates

/v1/affiliates/me/{stats,referrals,commissions,payouts,balance,withdraw,tree} (JWT) — self-service affiliate accounts, referral links, NET-15 payouts.

Admin (/v1/admin/*, role admin)

~80 endpoints. Notable groups: services (API-key reveal/rotation), products, prices, orders (refund), subscriptions (force-cancel), customers, entitlements, credits, affiliates + referral levels, payout/payment providers, promo codes, funnel analytics (funnel/{pipeline,metrics,timeseries,abandoned}), plus:

GroupEndpoints
MeteringGET/POST/PUT/DELETE /v1/admin/metering/{features,quotas}, GET /v1/admin/metering/usage
OutreachGET /v1/admin/outreach, /stats, /:id, POST /test-email
RiskGET /v1/admin/risk/{flagged,stats}, POST /v1/admin/risk/:id/review ({resolution: ok|fraud, refund?})
AI insightsPOST /v1/admin/insights/funnel (cached 6h, force to regenerate)

Provider webhooks (public, signature/shared-secret per rail)

POST /webhooks/{one,lava,stripe,apple,google,crypto}

Outbox events (billing → your service)

Every active Service with a webhookUrl receives all billing.* events as HTTP POST (headers x-service-code, x-event-type, x-event-id):

billing.payment.{status_changed,succeeded,failed,refunded}
billing.entitlement.{granted,revoked}
billing.subscription.{updated,payment_failed,cancelled,ended,trial_started}
billing.affiliate.payout.created
billing.quota.warning
billing.risk.flagged

Examples

Create a checkout session:

curl -X POST http://localhost:3000/v1/checkout/sessions \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -H "idempotency-key: unique-key-123" \
  -d '{"priceCode": "club_member_monthly", "mode": "SUBSCRIPTION"}'
# → { "sessionId": "...", "checkoutUrl": "https://billing.inite.ai/checkout/..." }

Metered credit consume (from an INITE module):

curl -X POST http://localhost:3000/v1/credits/consume \
  -H "x-api-key: sk_..." -H "Content-Type: application/json" \
  -d '{"userId": "u-123", "featureCode": "ai.chat.tokens", "units": 15000, "modelTier": "haiku"}'
# → { "success": true, "remainingBalance": 940, "creditsCharged": 4 }

Contract stability

Machine-to-machine endpoints are consumed by every INITE module: response shapes only ever grow (strict supersets), request fields are added as optional, error semantics don't change (consume returns 200 {success:false} for both insufficient credits and quota hits — it never started throwing). Breaking changes require a /v2 route.