Pharos Docs
Cobros

Cart Checkout

Crea un carrito controlado por tu backend con Checkout Sessions y checkout alojado.

Usa Checkout Sessions cuando tu backend controla un carrito o intento de compra concreto. La sesión puede referenciar precios del catálogo con priceId o guardar precios inline con priceData sin crear productos, planes o precios.

Conserva x-api-key en tu backend. No llames /api/v1 desde código del navegador ni aceptes precios, recurrencia o empresa como datos autorizados enviados por el cliente.

Convenciones de la API

Crea claves de API separadas con los permisos mínimos necesarios. Las consultas requieren READ; crear, actualizar, eliminar, vencer, cancelar y reembolsar requieren WRITE. Una clave válida sin el permiso de la operación devuelve 403; una clave ausente, inválida o vencida devuelve 401.

Lista Productos, Enlaces de pago y Checkout Sessions con limit (20 por defecto, 100 como máximo), startingAfter o endingBefore. Usa un solo cursor y toma el siguiente desde meta.lastId o el anterior desde meta.firstId. Todas las listas devuelven { data, meta }.

Todos los errores devuelven { error: { code, message, details? }, correlationId }. Registra el identificador de correlación junto con el identificador de tu solicitud. Reintenta sólo los errores seguros para la operación y reutiliza el mismo Idempotency-Key; consulta el recurso actualizado antes de reintentar un conflicto de versión. Revisa la Referencia de API para conocer estados y esquemas de cada operación.

Arquitectura

Navegador del comprador
    → tu backend
        → Pharos /api/v1/checkout-sessions
            → checkout alojado /checkout/session/:id
                → cálculo, factura, suscripción y pago
                    → webhook firmado a tu backend

Tu backend arma el carrito y modifica la sesión. Pharos administra el checkout y los registros comerciales. El navegador sólo necesita la url devuelta.

Flujo de compra

  1. El navegador pide checkout a tu backend usando la identidad del carrito, no precios autorizados por el navegador.
  2. Tu backend crea una sesión con POST /api/v1/checkout-sessions y un Idempotency-Key estable para esa operación.
  3. Guarda id, version, ETag y url asociados al carrito.
  4. Si necesitas modificarlo, hacelo mientras la sesión esté open y sin preparar. Cada operación usa su propia clave y el último ETag en If-Match.
  5. Devuelve la URL al navegador para abrir el checkout alojado.
  6. El comprador completa datos y medio de pago. Los cambios de cantidad respetan los límites guardados por el servidor.
  7. Pharos calcula el resultado y crea cliente, factura, suscripción y pago según corresponda.
  8. El pago puede ser inmediato, requerir acción o quedar pendiente.
  9. Las URLs de éxito y cancelación controlan navegación; no prueban un pago ni vencen la sesión.
  10. Reconcilia por checkoutSession.id y clientReferenceId y confirma la compra sólo con purchase_succeeded verificado y deduplicado.

Crear una sesión en el backend

const pharosApi = 'https://api.example.com/api/v1'

type Cart = {
  id: string
  items: Array<{ priceId: string; quantity: number }>
}

export async function createCheckout(cart: Cart) {
  const response = await fetch(`${pharosApi}/checkout-sessions`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': process.env.PHAROS_API_KEY!,
      'idempotency-key': `cart:${cart.id}:checkout-session`,
    },
    body: JSON.stringify({
      mode: 'PAYMENT',
      theme: 'light',
      clientReferenceId: cart.id,
      lineItems: cart.items.map((item) => ({
        ...item,
        adjustableQuantity: {
          enabled: true,
          minimum: 1,
          maximum: 10,
        },
      })),
      successUrl: 'https://shop.example/success?session={CHECKOUT_SESSION_ID}',
      cancelUrl: 'https://shop.example/cart',
    }),
  })

  if (!response.ok) {
    throw new Error(await response.text())
  }

  return {
    session: await response.json(),
    etag: response.headers.get('etag'),
  }
}

Deriva la clave de creación del carrito o intento. Repetir la misma operación con la misma clave devuelve la misma sesión lógica. Reutilizarla con un cuerpo distinto causa un conflicto de idempotencia.

Elegir catálogo o precios inline

Usa priceId cuando la oferta ya existe en el catálogo. Usa priceData para guardar un precio contextual definido por tu backend:

const customLineItem = {
  priceData: {
    currency: 'USD',
    unitAmount: '25.00',
    productData: {
      name: 'Custom support package',
      description: 'Configured for this order',
    },
    recurring: {
      interval: 'MONTH',
      intervalCount: 1,
      trialDays: 7,
    },
  },
  quantity: 1,
}

Los datos inline quedan guardados en la sesión. El navegador del checkout no puede reemplazar importes unitarios ni recurrencia.

Modificar un carrito abierto

export async function addItem(sessionId: string, etag: string, priceId: string) {
  const response = await fetch(`${pharosApi}/checkout-sessions/${sessionId}/line-items`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': process.env.PHAROS_API_KEY!,
      'idempotency-key': crypto.randomUUID(),
      'if-match': etag,
    },
    body: JSON.stringify({
      lineItems: [{ priceId, quantity: 1 }],
    }),
  })

  if (response.status === 409) {
    throw new Error('Reload and reconcile the session before retrying')
  }
  if (!response.ok) {
    throw new Error(await response.text())
  }

  return {
    session: await response.json(),
    etag: response.headers.get('etag'),
  }
}

Después de una modificación exitosa, actualiza la sesión, version y ETag guardados. Ante 409 SESSION_VERSION_CONFLICT, consulta la sesión, reconcilia el carrito y crea una modificación nueva. No repitas a ciegas datos de una versión vieja.

Dentro de /api/v1 también puedes usar:

  • PATCH /checkout-sessions/{id} para configuración no monetaria admitida, como tema, redirecciones, email, referencia o metadata.
  • PATCH /checkout-sessions/{id}/line-items/{itemId} para cantidad o límites ajustables.
  • DELETE /checkout-sessions/{id}/line-items/{itemId} para quitar un ítem permitido.
  • POST /checkout-sessions/{id}/expire para impedir otro uso del carrito.

Las modificaciones de API requieren una sesión abierta cuyo snapshot no esté bloqueado por la preparación del pago.

Redirigir desde el navegador

const response = await fetch('/api/cart/checkout', { method: 'POST' })
if (!response.ok) throw new Error('Checkout could not be created')

const { url } = await response.json()
window.location.assign(url)

Tu handler /api/cart/checkout realiza la llamada autenticada a Pharos. No expongas la clave de API en la respuesta ni en código del cliente.

Estados y confirmación

El status puede ser open, complete o expired; describe el flujo del comprador. paymentStatus puede ser unpaid, processing, paid, failed o no_payment_required; describe el cobro.

La redirección de éxito y checkout_session_completed no son señales de entrega de la compra. Consulta la sesión para reconciliar sus registros y confirma sólo con purchase_succeeded firmado. Un flujo gratuito o de prueba requiere tu política de acceso y el contrato de eventos correspondiente.

Resolver fallos

  • Ante fallos de red, repite con la misma clave sólo la operación exacta.
  • Ante 400, corrige la petición o composición del carrito.
  • Ante 401 o 403, revisa clave, entorno y permisos.
  • Ante 404, verifica que clave y sesión correspondan a la misma empresa.
  • Ante 409, distingue conflicto de idempotencia y de versión y reconcilia antes de reintentar.
  • Si el snapshot está bloqueado, crea otro intento en lugar de modificarlo.
  • Si el pago está en proceso, espera el webhook firmado.

Configuración comercial

Consulta Productos y precios, Cupones, Promociones y Descuentos. Las condiciones por producto requieren identidades coincidentes del catálogo; un nombre inline igual no garantiza elegibilidad. Usa sólo opciones admitidas por la API, sin suponer que cada ajuste del panel tiene un equivalente público.

Consulta Suscripciones y Facturas para interpretar los registros creados.

Continuar