Reference
API reference
HTTP endpoints exposed by the hosted backend. Auth, contact, checkout.
The hosted version of Mayva runs a backend (Fastify) on EC2/DO. The marketing site (this site) talks to it for auth, contact submission, and Stripe checkout creation. Self-host doesn’t expose any HTTP — the dashboard talks to core in-process.
This page is the contract between the marketing site and the backend. The backend is in a sibling repo.
Base URL
Production: https://api.mayva.ai
Staging: https://api.staging.mayva.ai
Authentication
Endpoints that require auth read a session cookie (mayva_session) set by /auth/login. The cookie is HttpOnly Secure SameSite=Lax. There is no token-based auth for the marketing site → backend hop.
POST /auth/signup
Creates an account.
Body:
{
"email": "user@example.com",
"password": "string >= 12 chars",
"name": "Display Name",
"plan": "indie" | "team"
}
Responses:
201—{ user: { id, email, name }, sessionExpiresAt }. Cookie set.409—{ error: "EMAIL_IN_USE" }.422—{ error: "VALIDATION", fields: {...} }.
POST /auth/login
Body: { email, password, remember?: boolean }.
Responses:
200—{ user, sessionExpiresAt }. Cookie set.401—{ error: "INVALID_CREDENTIALS" }.423—{ error: "LOCKED", retryAfter: seconds }. After 10 failed attempts in 15 min.
POST /auth/logout
No body. Returns 204. Cookie cleared.
GET /auth/me
Returns the current user.
Responses:
200—{ user }.401—{ error: "UNAUTHENTICATED" }.
POST /contact
Public, no auth. Rate-limited per IP. Requires a valid Turnstile token.
Body:
{
"name": "string",
"email": "string (email)",
"topic": "general" | "sales" | "privacy" | "security" | "team",
"message": "string >= 10 chars",
"turnstile": "string (token from cf-turnstile)"
}
Responses:
202—{ ok: true }. Email sent via Resend; reply expected within 24h.400—{ error: "TURNSTILE_FAILED" | "VALIDATION" }.429—{ error: "RATE_LIMITED", retryAfter }. 5/min/IP, 50/day/IP.
The marketing site uses an Astro endpoint /api/contact that proxies to this, attaching the Turnstile secret server-side.
POST /billing/checkout
Creates a Stripe Checkout session. Requires auth.
Body: { plan: "indie" | "team", seats?: number }.
Responses:
200—{ url: "https://checkout.stripe.com/..." }. Redirect the browser.400—{ error: "INVALID_PLAN" }.
The marketing site’s /api/checkout Pages Function calls this and 302s the browser to the returned URL.
POST /billing/portal
Creates a Stripe Customer Portal session for managing subscription / payment method. Requires auth.
Responses: 200 — { url }.
POST /billing/webhook
Stripe webhook receiver. Verifies signature, processes events. Not called by the marketing site directly — Stripe calls it.
Events handled: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed.
Error format
Every non-2xx response follows:
{
"error": "MACHINE_CODE",
"message": "Human-readable, safe to display"
}
Some errors include extra context (fields, retryAfter, etc.) but the error and message keys are always present.
Versioning
There is no version in the path. Breaking changes will introduce a new path (e.g. /auth/v2/login); the old path stays alive for at least 6 months. We expect breaking changes to be rare — this surface is small.