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 cuenta 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.

IDs de recursos

Los IDs de Pharos son cadenas opacas con prefijo de recurso, como cs_…, price_…, cus_… o pi_…. Guárdalos y envíalos sin modificarlos; no interpretes su sufijo. Durante la ventana de compatibilidad, la API también acepta un CUID histórico devuelto antes de este formato.

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 método 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 invoice.paid 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.

Asociar un cliente conocido

Envía customer con el ID de un Customer existente cuando tu aplicación ya conoce al comprador. Pharos valida que pertenezca a la misma cuenta y entorno y completa el checkout alojado con el perfil guardado. Si envías customer, omite customerEmail; usa customerEmail para un invitado cuyo perfil deba resolverse o crearse durante el checkout.

customerUpdate define qué campos guardados puede reemplazar el comprador durante el checkout. Los campos configurados como auto se guardan automáticamente en el Customer cuando el checkout se completa correctamente. Las tarjetas elegibles se guardan automáticamente después de que el comprador acepta los términos obligatorios del checkout. savedPaymentMethodOptions.redisplay: "enabled" controla si los métodos guardados aparecen en sesiones posteriores; la opción heredada save se acepta por compatibilidad, pero ya no cambia el comportamiento del checkout.

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

Para un cobro directo desde tu backend, creá un PaymentIntent con POST /api/v1/payment_intents. No cobra por defecto: enviá confirm: true o llamá a /api/v1/payment_intents/{id}/confirm para crear un Charge. Consultá un Charge por ID y creá un Refund separado con POST /api/v1/refunds.

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 invoice.paid 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 cuenta.
  • 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

Analítica de compras

Después de verificar invoice.paid, consulta la Checkout Session desde tu backend. customerDetails contiene los datos capturados del comprador; paymentDetails informa el pago exitoso y las cuotas efectivas. discountDetails.coupon mantiene el cupón separado de promotions. El value del cupón es su porcentaje o importe fijo configurado; discountAmount es el descuento monetario aplicado. Los importes usan unidades monetarias principales. Un discountDetails null indica que una sesión anterior no tiene copia guardada; { "coupon": null } confirma que no hubo cupón. No infieras crédito a partir de CARD, cuotas desde allowedInstallments ni cupón desde el descuento total. Las copias están disponibles para sesiones preparadas después de la migración y la publicación del servicio.