Objetivo

Trasladar una selección de plan a través de la frontera entre la landing (visitante, aún sin sesión) y el dashboard (usuario autenticado a punto de ser redirigido a Stripe Checkout). El endpoint firma con HMAC-SHA256 un payload (plan_slug, currency, billing_interval, exp) y lo guarda en la cookie app_checkout_intent. Tras verificar su teléfono via OTP, el RegistrationController lee la cookie, valida firma + TTL y crea la Checkout Session automáticamente.

Este es el único endpoint /v1/users/me/* que no requiere autenticación — el visitante aún no se ha registrado al momento de elegir un plan.

Prerrequisitos

  • Un slug de plan obtenido de GET /v1/plans/public (por ejemplo pro, starter).
  • Un origen de sitio autorizado a fijar cookies first-party bajo .example.com (o el dominio raíz donde sirvas tu landing).

Pasos

1. Firmar la intención

curl -X POST 'https://api.example.com/v1/users/me/subscription/checkout-intent' \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{
    "plan_slug": "pro",
    "currency": "MXN",
    "billing_interval": "month"
  }'

La respuesta fija la cookie app_checkout_intent:

Set-Cookie: app_checkout_intent=<payload>.<hmac>;
            Domain=.example.com;
            Max-Age=1800;
            HttpOnly; SameSite=Lax; Secure

Cuerpo (200):

{
  "data": {
    "type": "checkout_intent",
    "attributes": {
      "plan_slug": "pro",
      "currency": "MXN",
      "billing_interval": "month",
      "expires_at": "2026-05-28T11:30:00Z"
    }
  }
}

2. Continuar al flujo de registro

Tras firmar la intención, redirige al visitante a /register. Cuando verifica su teléfono via OTP, el paso post-OTP lee la cookie, valida HMAC + TTL e inicia la Checkout Session de Stripe sin ningún round-trip adicional.

La cookie se borra al consumirse — si el usuario re-entra al flujo, pídele que elija el plan otra vez.

Integración con Stripe.js

Típicamente no llamas confirmCardPayment en este paso. Este endpoint solo firma la intención; la Checkout Session real se crea server-side en el handler de verify-OTP del registro, que después redirige a session.url. El único trabajo de la landing es llamar a este endpoint cuando el visitante clickee "Elegir Pro / Elegir Starter" y dejar que la cadena de redirects haga el resto.

Errores

EstadoCódigoCausa
422billing_plan_slug_invalidFalta plan_slug o no cumple [a-z0-9_-]{1,50}.
422billing_intent_invalid_shapecurrency o billing_interval fuera del enum (MXN/USD, month/year).
429rate_limitedDemasiadas firmas desde la misma IP. Reintenta tras el header Retry-After.

Notas

  • El endpoint no valida que el plan exista en la base de datos. Un slug fantasma se firma normalmente; el flujo post-OTP falla en silencio (sin redirect a Stripe) y el usuario queda en el dashboard. Si necesitas mejor UX, valida slugs contra GET /v1/plans/public client-side antes de hacer el POST.
  • El TTL es 30 minutos (exp = now + 1800). Replay fuera de esa ventana devuelve 400 intent_expired en el paso post-OTP.
  • En producción la cookie lleva Secure. En dev local no, así que probar contra http://localhost funciona sin TLS.
  • Ver ADR-0024 (Stripe Billing Architecture) para el flujo Stripe completo y ADR-0090 (URL Convention) para el razonamiento detrás del path /v1/users/me/subscription/checkout-intent.

Nuevo en v1.49.6 — ver ADR-0090.