Tienda en línea

Crafty puede vender en cualquier sitio sin pasarela de pago: el comerciante acepta pago contra entrega, pago en la recogida o transferencia bancaria, y confirma el pago en el panel. Los pagos con tarjeta a través de la pasarela propia del comerciante (CardNET, AZUL) llegarán en una etapa posterior y se conectan a los mismos pedidos.

El frontend nunca calcula precios ni toca datos de pago. Muestra los productos, mantiene un carrito de slugs y cantidades, pide a Crafty los totales y envía al visitante al checkout alojado de Crafty (checkout.crafty.do/{store}), donde introduce sus datos, elige entrega o recogida y un método de pago, y recibe una página de pedido y un correo electrónico.

Cómo encaja todo

Pieza Dónde Qué hace
Plan Crafty central (store_enabled) El plan del espacio de trabajo debe incluir la tienda en línea.
Ajustes de la tienda Panel → sitio → pestaña Tienda, o la herramienta MCP configure_store (administradores) Activa la tienda por entorno; moneda, impuestos, entrega, recogida, métodos de pago, correos de pedidos, URL de retorno, color del checkout, captcha.
Productos La colección productos (Panel → Tienda → Productos, o herramientas de entradas) Una entrada publicada por producto.
Carrito y totales Tu frontend, con el SDK createCart() mantiene el carrito; crafty.orders.quote() lo valora.
Checkout checkout.crafty.do/{store}/c/{session} (alojado por Crafty) Datos del cliente, entrega o recogida, método de pago, captcha; realiza el pedido.
Página de pedido checkout.crafty.do/{store}/o/{number}?t={token} Estado, líneas, totales e instrucciones de pago; enlazada desde los correos.
Pedidos Panel → Tienda → Pedidos Confirmar, marcar como listo, entregado o cancelado, marcar pagado, nota interna, enviar WhatsApp al cliente.
Integraciones Webhooks de contenido order.created, order.updated, order.status_changed, order.paid Notifican a otros sistemas (ERP, entrega digital, Slack).

1. Activa la tienda

En el panel: sitio → Tienda → elige el entorno → Activar tienda. A través de MCP (administradores):

configure_store {
  "site_id": 3,
  "enabled": true,
  "currency": "DOP",
  "pickup": { "enabled": true, "address": "Av. Sarasota 55, Santo Domingo" },
  "delivery": { "enabled": true, "fee": "250", "free_from": "4000", "tax": "itbis-18", "zone": "Gran Santo Domingo" },
  "payment_methods": { "cash_on_delivery": true, "pay_at_pickup": true, "transfer": true },
  "transfer_accounts": "Banco Popular · Cuenta corriente 812-345678-9 · Café Monteverde SRL",
  "notify_emails": ["[email protected]"],
  "return_url": "https://monteverde.do/tienda"
}

Al activarla:

  • crea la colección productos o adopta una colección productos existente cuando sus campos de tienda tienen los tipos indicados abajo (si no, informa de los campos en conflicto);
  • asigna al entorno un identificador de tienda y su URL de checkout (checkout.crafty.do/<site-slug> para producción, <site-slug>-<environment> para los demás);
  • define un prefijo de pedidos a partir de las iniciales del sitio (CM- → CM-1001) y la plantilla fiscal dominicana (ITBIS 18 %, ITBIS 16 %, Exento).

Desactivarla detiene los pedidos nuevos; los productos y los pedidos se conservan. Envía configure_store solo con site_id para leer los ajustes actuales, la URL del checkout y cuántos productos no tienen precio.

2. Productos

Los productos son entradas de productos. Mientras la tienda esté activa, estos campos pueden renombrarse, pero no eliminarse ni cambiar de tipo (el cálculo de precios los usa), y la colección no puede borrarse. Añade los demás campos que necesites: nunca cambian el precio.

Campo Tipo Notas
nombre cadena, obligatorio Nombre del producto (también el título de la entrada y la fuente del slug).
precio decimal, obligatorio Precio en la moneda de la tienda, p. ej. 1450 o 1450.50. Incluye impuestos salvo que la tienda indique lo contrario.
disponible selección, obligatorio disponible, pocas (quedan pocas) o agotado (sin existencias: no puede comprarse).
variantes repetidor Cada variante: nombre (obligatorio), precio (solo cuando cambia el precio), sku. Un producto con variantes debe comprarse con una de ellas.
imagenes multiReference → Media La primera imagen aparece en el checkout y en el pedido.
sku cadena
impuesto cadena Una clave fiscal de la tienda (itbis-18, itbis-16, exento). Vacío usa el impuesto predeterminado de la tienda.

Solo los productos publicados pueden comprarse. Las traducciones de nombre (mediante upsert_translation / translate_content) se muestran en el checkout en el idioma del visitante.

3. El carrito y los totales en el frontend

Las llamadas a la tienda necesitan siteSlug en la configuración del cliente. La clave de API, cuando se envía, elige su entorno; sin ella, la tienda usa producción.

import { createCraftyClient } from "@craftware/crafty-sdk";
import { createCart } from "@craftware/crafty-sdk/cart";

const crafty = createCraftyClient({ baseUrl, apiKey, siteSlug: "monteverde" });
const cart = createCart({ key: "monteverde:cart" });

const store = await crafty.store.get();
// { enabled, currency, prices_include_tax, taxes, delivery, pickup, payment_methods, checkout_url, … }

const { data: products } = await crafty.entries.list("productos", { per_page: 100 });

cart.add("geisha-lavado", { variant: "Molido para filtro", quantity: 2 });
cart.subscribe((items) => renderCartBadge(items));

const quote = await crafty.orders.quote(cart.items(), { fulfillment: "delivery" });
// quote.items, quote.subtotal, quote.delivery_fee, quote.taxes, quote.total (cadenas decimales)
// quote.unavailable: líneas que Crafty omitió, con motivo not_found | sold_out | variant_not_found | variant_required | no_price

Muestra quote.total, no un total que tú sumes: Crafty aplica los precios de las variantes, el umbral de entrega gratuita y redondea cada impuesto una vez por pedido, exactamente como se cobrará el pedido. crafty.store.get() se guarda en caché durante un minuto; quote() siempre es fresco.

4. Checkout

await crafty.checkout.start(cart.items(), {
  returnUrl: "https://monteverde.do/gracias", // opcional; por defecto, la URL de retorno de la tienda
  cancelUrl: "https://monteverde.do/carrito", // opcional: "Volver al carrito" antes de pedir; sin ella, el enlace va a returnUrl
  fulfillment: "pickup", // opcional: la elección del carrito, seleccionada al abrir el checkout
  fields: { dedicatoria: "Feliz cumpleaños" }, // opcional: rellena los campos propios del checkout por clave
});

start() crea una sesión de checkout (válida 24 horas) y envía el navegador al checkout alojado. checkout.create() devuelve { id, url, expires_at } sin redirigir, para SSR o enlaces. El checkout vuelve a calcular el precio del carrito; si un precio cambió desde que se cargó la página, muestra el nuevo total y pide al cliente que lo confirme.

Tras el pedido, la página de pedido muestra un botón Volver a {tienda} hacia la URL de retorno con ?order=<number>, y lleva allí por sí sola después de tres minutos. En esa página llama a cart.clearOnReturn(): vacía el carrito y devuelve el número de pedido para que puedas dar las gracias al cliente.

El visitante puede cerrar la pestaña en su lugar. Para vaciar el carrito igualmente, guarda la sesión al iniciar el checkout y compruébala en cada carga de página:

await crafty.checkout.start(cart.items(), { onSession: (session) => cart.rememberCheckout(session.id) });

// al cargar la página
await cart.syncCheckout(crafty); // vacía el carrito cuando ese checkout se convirtió en pedido

syncCheckout() consulta GET /api/v1/sites/{site}/checkout/{id} (solo { placed }) y conserva el carrito si sus líneas cambiaron después de iniciar el checkout.

Un pedido puede leerse de nuevo con su token, que está en el enlace de la página de pedido (?t=):

const order = await crafty.orders.get("CM-1001", token);

Cupones de descuento

El comerciante crea los códigos en el panel (Cupones), con upsert_coupon o POST /api/v1/coupons. Un cupón por pedido:

  • Tipos: percent (con un max_discount opcional), fixed (un importe) o free_delivery.
  • Un cupón se aplica a todos los productos o a algunos elegidos (applies_to: "entries" más los slugs de producto).
  • Límites opcionales: min_subtotal (el subtotal de productos antes de la entrega), max_uses, max_uses_per_email, starts_at y ends_at (días completos en la hora de la tienda), e is_active.

El visitante escribe el código en el checkout alojado. Este muestra el descuento o el motivo por el que el código no aplica.

  • El descuento se reparte entre las líneas antes de impuestos.
  • "Entrega gratis desde" mira el subtotal después del descuento.
  • Un pedido cancelado devuelve su uso.
  • Un pedido sin nada pendiente de pago se marca como pagado con payment_method: "none".

Para llevar un código desde un enlace compartido (https://monteverde.do/?cupon=BIENVENIDA10) al checkout:

cart.captureCoupon(); // al cargar la página: guarda ?cupon= (o ?coupon=) en el carrito
const quote = await crafty.orders.quote(cart.items(), { coupon: cart.coupon() });
// quote.discount_total, quote.delivery_discount, quote.coupon, quote.coupon_error { reason, message, missing }
await crafty.checkout.start(cart.items(), { coupon: cart.coupon() });

Muestra quote.coupon_error.message tal cual: ya está en el idioma del visitante. Valores posibles de reason:

  • not_found
  • paused
  • not_started
  • expired
  • exhausted
  • used_by_email
  • minimum (con missing, el importe que aún necesita el carrito)
  • not_eligible
  • too_many_attempts

Los códigos fallidos tienen límite de intentos por visitante. El carrito olvida el código una vez realizado el pedido. Los pedidos, los webhooks y orders.get() incluyen discount_total, delivery_discount, coupon y el discount de cada línea.

5. Pedidos y notificaciones

  • Cada pedido recibe un número por entorno (<prefix>1001, 1002, …), con estado Nuevo y pago Pendiente.
  • El estado solo avanza: nuevo → confirmado → listo → entregado; cualquier pedido abierto puede cancelarse.
  • Marcar pagado registra el pago (con una referencia opcional, p. ej. el número de la transferencia).
  • El cliente recibe un correo en cada paso (con el nombre de la tienda; las respuestas van al comerciante); los notify_emails reciben todos los pedidos nuevos. Desactiva los correos al cliente con customer_emails: false.
  • Los editores ven Pedidos solo con el permiso de sección orders (oculto, lectura o edición), que se configura por usuario en Ajustes → Usuarios.
  • Editar (panel), update_order (MCP) o PATCH /api/v1/orders/{number} cambian líneas, cantidades, entrega, método de pago, cliente, dirección, campos del checkout y metadatos mientras el pedido esté sin pagar y no esté cancelado ni entregado; una address enviada con solo algunas claves cambia solo esas. Las líneas ya vendidas conservan su precio; las nuevas toman el precio de hoy; la entrega y los impuestos se recalculan. Cada edición se guarda en el historial del pedido, envía un correo al cliente (salvo notify: false) y dispara el webhook order.updated. Los metadatos aún pueden cambiar en un pedido pagado, sin enviar correo.
  • Los pedidos pueden crearse en el panel (Nuevo pedido, p. ej. un pedido telefónico), con create_order o POST /api/v1/orders. Su source es panel, mcp o api; los campos obligatorios del checkout no se exigen allí.
  • Exportar CSV descarga los pedidos que coinciden con los filtros de la lista (UTF-8, se abre en Excel): líneas, totales, campos del checkout, metadatos y nota interna.
  • Los correos a clientes llevan el logotipo y el color de la tienda; Crafty solo aparece como una pequeña marca gris al pie.

Campos del checkout, aspecto y analítica

Se configura en el panel (Tienda) o con configure_store:

  • checkout_fields: hasta 15 campos extra que se piden antes del pago, con los tipos de campo de formulario (text, textarea, email, tel, number, select, checkbox, radio, date). Cada uno tiene key, label, type, required, show (always, delivery o pickup), help_text opcional, options (select/radio) y translations por idioma ({ en: { label, help_text, options } }). Cada pedido guarda una copia (fields: [{ key, label, type, value }]), que se muestra en el panel, los correos, el CSV y los webhooks. Los valores de selección se guardan en el idioma predeterminado.
  • logo_media_id (una imagen de los medios del sitio), color, font (system, inter, dm-sans, poppins, lora, playfair) y corners (square, rounded, soft) dan estilo al checkout, a la página de pedido y a los correos a clientes.
  • checkout_analytics: true carga el GA4 y el Meta Pixel del entorno (desde configure_analytics) en el checkout, disparando begin_checkout / InitiateCheckout y, una vez por pedido, purchase / Purchase con el número de pedido como id de transacción. Tag Manager, TikTok y el código personalizado del head nunca se ejecutan en el checkout, para que los scripts de un sitio no puedan leer datos de clientes allí. La página de pedido en sí no carga analítica, porque su URL contiene el token del pedido: purchase se dispara en una página breve intermedia, y un enlace de checkout ya usado deja de abrir el pedido. Añade el dominio del checkout a la lista de dominios cruzados de GA4 para mantener unidas las sesiones.

Endpoints REST

No se requiere clave de API; el sitio va en la URL. Los errores usan la forma habitual { error, message }.

Método Ruta Notas
GET /api/v1/sites/{site}/store Ajustes públicos.
POST /api/v1/sites/{site}/orders/quote Cuerpo { items: [{ slug, variant?, quantity }], fulfillment?: "delivery" | "pickup", locale?, coupon?, email? }.
POST /api/v1/sites/{site}/checkout Cuerpo { items, return_url?, cancel_url?, fields?, fulfillment?, locale?, coupon? } → 201 { data: { id, url, expires_at } }.
GET /api/v1/sites/{site}/orders/{number} Token en ?token= o X-Order-Token.
GET /api/v1/sites/{site}/checkout/{id} { data: { placed } }: indica si la sesión se convirtió en pedido; 404 una vez eliminada.

Una tienda cerrada responde 404 STORE_DISABLED. Los endpoints de cotización y checkout tienen límite de peticiones por visitante.

API de pedidos autenticada

Para un ERP, TPV o backend: un token OAuth (el mismo que MCP) con crafty:read para leer y crafty:write para crear o modificar; los editores necesitan el permiso orders. Envía site (slug) y, opcionalmente, environment (por defecto production) en la query o el cuerpo.

Método Ruta Notas
GET /api/v1/orders Filtros status, payment_status, metadata[erp_id]=… (hasta 5 pares), per_page (máx. 100).
POST /api/v1/orders Cuerpo como create_order: customer, fulfillment, address?, payment_method, items, fields?, metadata?, locale?, expected_total?, coupon? → 201.
GET /api/v1/orders/{number} El pedido con payment_url (la página de pedido del cliente), fields, metadata, next_statuses y events.
PATCH /api/v1/orders/{number} Claves de edición (items con id para líneas existentes, quantity: 0 elimina, coupon para añadir, mantener o quitar un código con null, coupon_below_minimum: "keep" | "remove"), además de status, paid, payment_reference, internal_note, notify. Devuelve el pedido y applied.
GET /api/v1/coupons Los cupones del entorno con status, uses, orders, sales y discounted. Los editores necesitan el permiso coupons.
POST /api/v1/coupons Cuerpo como upsert_coupon: code, type, value, max_discount?, min_subtotal?, applies_to?, products?, max_uses?, max_uses_per_email?, starts_at?, ends_at?, is_active? → 201.
GET / PATCH / DELETE /api/v1/coupons/{code} Leer, cambiar (solo las claves enviadas) o eliminar. Un cupón usado conserva su código y tipo, y DELETE lo pausa en su lugar.

Los rechazos usan { error, message } con el código (PRICE_CHANGED es 409 e incluye la nueva quote).

Starter

npm create crafty -- --template=astro-store crea un sitio de tienda en Astro: catálogo, páginas de producto con variantes, una página de carrito con totales en vivo y el checkout alojado. Crea la colección productos y productos de demostración, y activa la tienda cuando el plan del espacio de trabajo la incluye.

Errores comunes

  • Precios o impuestos calculados en el frontend. Muestra siempre los totales de quote(); el checkout cobra los números de Crafty.
  • Productos en borrador. Solo los productos publicados pueden comprarse; una compilación estática también lista solo entradas publicadas.
  • Un producto con variantes añadido sin ninguna. Vuelve en unavailable con variant_required.
  • Checkout 404. La tienda está desactivada, el plan no la incluye o la sesión caducó (24 h): inicia un nuevo checkout.
  • Mover el sitio a otro espacio de trabajo. Los pedidos y el identificador del checkout se mueven con él.