Cargando documentación…
Cargando documentación…
@notheadless/sdk/server
El navegador manda ids y cantidades. Tu servidor los valida contra datos en vivo y crea una sesión de checkout alojada en Tiendanube, que se encarga del pago y del envío.
import { createCheckoutHandler } from "@notheadless/sdk/server"
import { nube } from "@/lib/nube"
export const POST = createCheckoutHandler({
client: nube,
categoryId: 41184206, // pertenencia al catálogo
allowedOrigins: ["https://shop.example"], // por defecto: same-origin
getBuyerIp: (req) => req.headers.get("x-real-ip"), // rate limits por comprador
})Contrato del request y de la respuesta:
// POST /api/checkout
{ "lineItems": [{ "productId": 369801113, "variantId": 1605355917, "quantity": 1 }], "couponCode": "CAPY10" }
// 200
{ "checkoutUrl": "https://capi.tienda/checkout/v3/proxy/…" }
// 409: hay que revisar el carrito (el cliente lo conserva y muestra los problemas de cada línea)
{ "error": { "code": "cart_invalid", "issues": [
{ "code": "insufficient_stock", "productId": 369801255, "variantId": 1605356438, "maxQuantity": 10, "message": "Only 10 left…" }
] } }| Prop | Tipo | Descripción |
|---|---|---|
| Origen | 403 origin_forbidden | El header Origin tiene que coincidir con allowedOrigins (o con el host del propio request). Los requests sin Origin se rechazan. |
| Cuerpo | 400 invalid_request | Solo application/json, 16 KiB como máximo. Cupón de hasta 30 caracteres, sin caracteres de control. |
| Forma | invalid_line | productId, variantId y quantity tienen que ser enteros seguros y positivos. Todo lo demás, incluidos los precios, se ignora. |
| Pertenencia | product_not_in_catalog | El producto tiene que estar publicado y pertenecer a categoryId. |
| Propiedad | variant_not_found | La variante tiene que pertenecer a ese producto. |
| Precio | price_on_request | Las variantes sin precio (“Consultar”) no pueden pasar al checkout. |
| Cantidad | quantity_limit | Las variantes repetidas se suman; máximo 99 por variante. |
| Stock | out_of_stock / insufficient_stock | Cuando se controla el stock, quantity ≤ stock. maxQuantity le indica a la UI qué cantidad sí funcionaría. |
Los dos handlers aplican rate limiting por cliente para frenar el spam de creación de carritos: por defecto, 10/min para el checkout y 60/min para refrescar el carrito. La key sale de getClientKey; si no está, de getBuyerIp; y si tampoco, de un bucket compartido. Al pasarse del límite, el handler devuelve 429 con Retry-After, y CheckoutButton conserva el carrito y muestra un mensaje para reintentar.
import { createCheckoutHandler, memoryRateLimiter, type RateLimiter } from "@notheadless/sdk/server"
// un solo servidor: ventana deslizante en memoria
createCheckoutHandler({ client, rateLimit: memoryRateLimiter({ limit: 5, windowMs: 60_000 }) })
// serverless / varias instancias: conectá un store compartido
const upstash: RateLimiter = { limit: async (key) => { const r = await ratelimit.limit(key); return { ok: r.success, remaining: r.remaining, retryAfter: Math.ceil((r.reset - Date.now()) / 1000) } } }
createCheckoutHandler({ client, rateLimit: upstash, getClientKey: (req) => req.headers.get("x-real-ip") })x-forwarded-for se puede falsificar cuando los requests te llegan directo. El limiter en memoria es por proceso, así que en serverless usá un store compartido.createCartHandler vuelve a calcular los precios de un carrito contra datos en vivo y devuelve los problemas sin crear un checkout. El CartSheet de NotHeadless UI lo llama cada vez que se abre el carrito, así los cambios de precio y stock aparecen antes del checkout.
const cart = await nube.cart.validate(body.lineItems, { categoryId: 41184206 })
if (!cart.ok) return Response.json({ issues: cart.issues }, { status: 409 })
cart.lines // ValidatedLine[]: name, unitPrice, stock, maxQuantity, image
cart.subtotal // centavos, para mostrar; el checkout alojado calcula el precio final del pedido
const { checkoutUrl } = await nube.checkouts.create({ lineItems: cart.lineItems, couponCode })
return Response.redirect(checkoutUrl, 303)checkouts.create combina las variantes repetidas, aplica el tope de 99, envía solo product_id / variant_id / quantity y verifica que la URL devuelta sea HTTPS.