Reference

API reference

HTTP endpoints exposed by the hosted backend. Auth, contact, checkout.

Updated · 2026-05-28 HOSTED ONLY

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.