La API de las tiendas cubobase (plan profesional): leer y cambiar pedidos y productos desde otro sistema, y recibir avisos al instante.
Nuevo Agentes de IA: cada tienda tiene su servidor MCP en https://app.cubobase.com/mcp. El dueño conecta su Claude o su ChatGPT tocando «Autorizar», sin llaves. Cómo funciona
El dueño de la tienda crea una llave en su app: Menú → Conexiones → Otro sistema → Crear una llave. La llave empieza con cb_ y se muestra una sola vez. Cada pedido a la API la lleva en el encabezado:
curl https://app.cubobase.com/api/v1/pedidos?estado=nuevo \
-H "Authorization: Bearer cb_TU_LLAVE"
moneda), como números.429.| Método | Dirección | Qué hace |
|---|---|---|
| GET | /api/v1/pedidos | La lista. Filtros: estado (uno o varios, separados por coma), pagado (si/no), desde (creados desde esa fecha), modificado_desde (para sincronizar: del más viejo al más nuevo), telefono, q (busca como el buscador de la app: cada palabra en el nombre, el correo, la dirección o la nota, sin importar tildes ni mayúsculas; un número también encuentra el pedido con ese número o el celular que lo tiene), pagina, por_pagina (hasta 200). Contesta {"pedidos": […], "pagina": 1, "hay_mas": false}. |
| GET | /api/v1/pedidos/{numero} | Un pedido, por su número (el que ven el dueño y el comprador). |
| POST | /api/v1/pedidos | Crea un pedido. Si se cobra por link (Mercado Pago o Stripe), lo trae en link_de_pago. |
| PATCH | /api/v1/pedidos/{numero} | Cambia el estado, si está pagado, el código de envío o los datos propios, y corrige el cliente, la entrega o lo que lleva. |
| POST | /api/v1/pedidos/{numero}/devolver | Le devuelve la plata al comprador, toda o una parte, por Mercado Pago o Stripe. |
{
"numero": 128, "estado": "preparando", "pagado": true, "pagado_el": "2026-10-10T19:13:02-03:00",
"creado": "2026-10-10T19:12:40-03:00", "actualizado": "2026-10-10T19:30:00-03:00", "origen": "tienda",
"cliente": {"nombre": "Martín P.", "telefono": "099 123 456", "email": "martin@ejemplo.com"},
"entrega": {"tipo": "envio", "envio": "DAC a domicilio", "direccion": "Rivera 2450, Montevideo",
"paga_el_que_recibe": false, "para_el": null, "para_hora": null,
"seguimiento": {"agencia": "DAC", "codigo": "870-13465148"}, "en_agencia": null,
"dac": {"entrega": "domicilio", "departamento": "10", "ciudad": "363", "barrio": "305",
"calle": "Rivera 2450", "agencia": null, "guia": "870-13465148"}},
"medio_pago": "mercadopago", "link_de_pago": null,
"items": [{"producto_id": 31, "variante_id": 88, "codigo": "JEAN-40", "nombre": "Jean recto",
"variante": "40", "precio": 2290, "cantidad": 1}],
"subtotal": 2290, "descuento": 0, "descuento_texto": "", "envio": 250, "total": 2540, "devuelto": 0,
"cobrado": 2540, "a_devolver": 0, "moneda": "UYU",
"avisos": {"recibido": "2026-10-10T22:13:05+00:00", "preparando": "2026-10-10T22:30:00+00:00"},
"nota": "", "motivo": "", "reclamo": null, "junto_con": null, "url": "https://malva.cubotienda.com/pedido/…", "datos": {}
}
Avisos al comprador: avisos dice qué mails (o avisos al celular) ya le salieron y cuándo: recibido, pago, preparando, listo, en_camino, rastreo, en_agencia, entregado, resena, retiro15/retiro45/retiro70 (los recordatorios de retiro), cancelado, devuelto, digital. "sin_aviso" en vez de la fecha: se dio por dado sin mandarlo (avisar: false). Cuando sale uno, actualizado cambia y llega el aviso al instante pedido.aviso.
La plata: cobrado es lo que pagó por Mercado Pago o Stripe desde la tienda (lo que se le puede devolver por acá), o null si no pagó así. devuelto, lo que ya se le devolvió. a_devolver, lo que le sobra porque el total bajó después de pagar (se pasó a retiro, se sacó algo, se dejó de cobrar el envío): cobrado − devuelto − total, o 0; está siempre al día, también si se corrigió desde la app, y es lo que se devuelve con …/devolver. En un pedido cancelado no cuenta todo lo pagado (un contracargo o lo devuelto desde Mercado Pago no pasan por cubobase): para cancelar y devolver todo, "devolver_plata": true.
Estados: nuevo → preparando → listo → en_camino (si se manda) → entregado; y dos finales: cancelado (con motivo) y devuelto (solo después de entregado). «Pagado» va aparte. Pasar a entregado lo marca pagado.
Reclamos de Mercado Pago: si el comprador abre un reclamo o desconoce la compra, reclamo es {"tipo": "reclamo", "cuando": …} y el pedido queda en espera (no conviene prepararlo ni mandarlo); si Mercado Pago lo resuelve a favor de la tienda, vuelve a null. Un contracargo, o un reclamo que gana el comprador, lo pasa a devuelto con reclamo {"tipo": "contracargo", …}.
POST /api/v1/pedidos
{
"cliente": "Lucía M.", "telefono": "099 123 456", "email": "lucia@ejemplo.com",
"items": [{"codigo": "VEST-01", "valores": ["M"], "cantidad": 1},
{"producto_id": 31, "variante_id": 88, "cantidad": 2},
{"codigo": "PERSONALIZADA", "precio": 800}],
"entrega": {"tipo": "envio", "envio_id": 3, "direccion": "Av. Brasil 2580, Montevideo"},
"medio_pago": "mercadopago",
"nota": "Para regalo",
"datos": {"id_en_mi_sistema": "A-5521"},
"avisar": true
}
producto_id o codigo. Si el producto viene en talles o colores, además variante_id o valores.422 con el motivo.precio vale eso, sin descuentos (una personalizada a 800, una reposición a 0). Sirve también para un producto «a consultar». El mismo producto no puede ir con dos precios distintos. Si todo el pedido da 0, queda pagado y sin link de pago.entrega: "retiro" o {"tipo": "envio", "envio_id": …, "direccion": …} (los envíos: GET /api/v1/envios).dac en GET /api/v1/envios): en vez de direccion, la dirección por partes, y la guía sale con un toque desde la app. A domicilio: "dac": {"departamento": "Montevideo", "ciudad": "Montevideo", "barrio": "Pocitos", "calle": "Av. Brasil 2580 apto 3"}; en la agencia: "dac": {"departamento": "Salto", "agencia": "Salto Terminal (Av. Batlle 2265)"}. Cada parte, con el código de DAC o con el nombre (sin importar tildes ni mayúsculas); un barrio de Montevideo escrito como ciudad también vale. El barrio es opcional, pero ayuda a DAC a encontrar la calle. Si algo no está en las listas de DAC, contesta 422 (direccion_invalida)."junto_con": 127 (el número del pedido que ya pagó el envío) o "sin_envio": true: el envío no se cobra de nuevo (envio 0), como «Sumarle cosas» en la app. El pedido lo muestra en junto_con (o null)."envio_al_recibir": true, solo con un envío de DAC. El envío no entra en el total, entrega.paga_el_que_recibe queda en true y la guía sale «paga el destinatario». Si el envío ya salía gratis (por el monto), sigue gratis. No va junto con sin_envio ni junto_con.medio_pago: uno de los de la tienda (GET /api/v1/tienda): mercadopago, stripe, transferencia, efectivo.avisar: false: no le llega ningún mail al comprador ni al dueño (por ejemplo, al pasar pedidos viejos). Y el «recibimos tu pedido» no sale tampoco después: queda en avisos como "sin_aviso".PATCH /api/v1/pedidos/128
{"estado": "en_camino", "seguimiento": {"agencia": "DAC", "codigo": "870-13465148"}}
PATCH /api/v1/pedidos/129
{"estado": "cancelado", "motivo": "Sin stock"}
PATCH /api/v1/pedidos/130
{"pagado": true, "datos": {"impreso": true}}
PATCH /api/v1/pedidos/128
{"en_agencia": {"donde": "SALTO", "cuando": "2026-10-27T17:12:51"}}
Quedó en una agencia: si la guía la hace tu sistema, cuando la transportista deja el paquete esperando en una agencia mandá en_agencia (donde: la agencia, hasta 80; cuando: opcional, sin zona se toma la hora de la tienda). Al comprador le llega el mail «Tu pedido llegó a la agencia, ¡ya podés retirarlo!», con la agencia, la empresa de seguimiento y el código de rastreo. Solo con un envío en en_camino (puede ir en el mismo cambio que lo pasa a en_camino; si no, 409). Se anota una sola vez: mandarlo de nuevo no repite el mail ni cambia la agencia. El pedido lo muestra en entrega.en_agencia ({"donde", "cuando"} o null). Con las guías que hace cubobase (DAC conectado), esto pasa solo.
Todo es opcional. al_stock: true con devuelto vuelve lo devuelto al stock. Si el estado no permite el cambio, contesta 409. avisar: false no le manda el aviso al comprador, ni ahora ni después: el aviso de ese cambio (por ejemplo, el «gracias por retirar» al pasar a entregado) queda en avisos como "sin_aviso" y no sale en el próximo cambio con aviso. Los avisos de otros pasos (el del pago, por ejemplo) no se tocan. Con "devolver_plata": true, al pasar a cancelado o devuelto le devuelve además todo lo que falta devolver (si se pagó con Mercado Pago o Stripe): primero la plata, y si no sale, no cambia nada.
PATCH /api/v1/pedidos/131
{"cliente": "Martín Pérez", "telefono": "099 765 432", "nota": "Tocar timbre",
"entrega": {"tipo": "envio", "envio_id": 3, "direccion": "Rivera 2450, Montevideo"},
"items": [{"codigo": "JEAN-40", "cantidad": 2}, {"codigo": "PERSONALIZADA", "precio": 800}]}
cliente, telefono, email, nota (hasta 400), entrega (como al crearlo, también con dac; direccion hasta 200), para_el, para_hora y items.items es la lista entera de lo que tiene que llevar, como al crearlo: lo que ya tenía cambia de cantidad (y de precio, si el renglón trae precio; si no, conserva el suyo), lo que no está se saca y lo nuevo se suma. El stock se mueve solo.entrega o adentro): "envio_al_recibir": true (solo DAC) el envío no entra en el total y entrega.paga_el_que_recibe queda en true; "sin_envio": true el envío queda en 0 y la guía sale por la cuenta de la tienda; false en cualquiera de los dos, se cobra en el total al precio de la tienda. Si no se manda ninguno, sigue como estaba: un pedido que pagaba al recibir o que no pagaba envío sigue así aunque cambie de agencia, de dirección o de envío; uno que pagaba el envío en el total se recalcula. Así un pedido ya pagado puede pasar de retiro a envío, o de agencia a domicilio, sin que suba el total. No van con retiro ni juntos (400). Ejemplo: {"entrega": {"tipo": "envio", "envio_id": 4, "dac": {"departamento": "Montevideo", "calle": "Rivera 2450"}}, "envio_al_recibir": true}.falta_cobrar; si se pagó con Mercado Pago o Stripe y el total baja, a_devolver dice cuánto sobra (se devuelve con …/devolver; si deja de cobrarse un envío que ya pagó, también). No se puede subir el total de lo pagado por Mercado Pago o Stripe: se cobra aparte (422; si lo que sube es el envío, el mensaje lo dice).409). En camino o entregado: solo los datos del cliente. Con la guía de DAC hecha: la entrega no (hay que anularla desde la app). Lo que no da (sin stock, sin dirección) contesta 422 con el motivo, y no cambia nada.POST /api/v1/pedidos/128/devolver
{"monto": 500, "motivo": "Llegó rayada", "clave": "dev-128-1"}
monto, devuelve todo lo que falta devolver. El pedido no cambia de estado.clave (hasta 60 letras, recomendada): la de tu sistema para esa devolución. Si la conexión se corta y se reintenta con la misma, no se devuelve dos veces: contesta 200 con "devolucion": {"monto": 500, "nueva": false}. Una devolución hecha contesta 201 con "nueva": true.avisar: false para que no). Si no se puede (más de lo que pagó, la cuenta desconectada, Mercado Pago dijo que no), contesta 422 no_se_puede_devolver con el motivo, y no sale nada.Cada pedido y cada producto tiene un casillero libre, datos: un objeto JSON para lo que el otro sistema necesite guardar (su número, una foto, una marca). Lo que se manda se suma a lo que había; una clave con null se borra. Hasta 16.000 caracteres en total. La tienda no lo muestra.
| Método | Dirección | Qué hace |
|---|---|---|
| GET | /api/v1/productos | La lista (también los ocultos). Filtros: q (busca en el nombre, el código y el detalle, sin importar tildes), codigo, id_antes, visible (si/no), pagina, por_pagina. |
| GET | /api/v1/productos/{id} | Un producto. |
| POST | /api/v1/productos | Crea uno (hace falta nombre y precio). |
| PATCH | /api/v1/productos/{id} | Cambia lo que se mande. |
| DELETE | /api/v1/productos/{id} | Lo saca de la tienda (los pedidos viejos lo siguen nombrando). |
POST /api/v1/productos
{
"nombre": "Vestido floreado", "codigo": "VEST-01", "precio": 2490, "precio_antes": 2990,
"detalle": "Viscosa, largo midi", "descripcion": "…", "visible": true,
"categorias": ["Ropa", "Novedades"],
"fotos_url": ["https://mi-sistema.com/fotos/vestido.jpg", "https://mi-sistema.com/fotos/vestido-espalda.jpg"],
"id_antes": "6207",
"opciones": [{"nombre": "Talle", "valores": ["S", "M", "L"]}],
"variantes": [{"valores": ["S"], "stock": 3}, {"valores": ["M"], "stock": 2, "codigo": "VEST-01-M"},
{"valores": ["L"], "stock": 0}],
"datos": {"id_en_mi_sistema": 6207}
}
PATCH /api/v1/productos/31
{"precio": 2590, "variantes": [{"valores": ["M"], "stock": 5}]}
nombre 120 letras, detalle 300 (un renglón), descripcion 3.000 (con renglones), codigo 60, cada categoría 60. Si uno no entra, contesta 400 dato_invalido diciendo cuál y su largo, y no cambia nada: nunca se guarda cortado.stock: un número, o null si no se lleva la cuenta. En los productos con opciones, el stock va en cada variante (y el del producto es la suma).categorias: por nombre; las que no existen se crean. La primera es la principal.fotos_url: todas las fotos, en orden (la primera es la principal); la tienda las baja (https, hasta 15 MB cada una) y reemplazan a las que había. Si una no se puede bajar, contesta 422 diciendo cuál y no cambia ninguna (y un producto nuevo no se crea). Si la foto está en tu disco y no en internet, va adentro del pedido: "data:image/jpeg;base64,…" en lugar de la dirección (JPG, PNG o WebP, hasta 15 MB). También sirve el nombre de una foto que ya es de la tienda ("12-a8f3c9e1b2c4d6f8.jpg"): se guarda una copia. foto_url (una sola) cambia solo la principal.fotos_valor: de qué color (u otro valor de una opción) es cada foto, para que la tienda muestre la de ese color: [{"foto": 1, "valor": "Azul marino"}], donde foto es el número (1 = la principal) o su dirección. Reemplaza lo que había; [] lo saca. Los valores son de una sola de sus opciones. El GET lo devuelve igual, con la url de cada foto.id_antes: el número del producto en la tienda de antes (en WooCommerce, su id). El catálogo de Meta y el píxel usan ese número, así los anuncios siguen reconociendo el producto al mudarse.opciones define los talles o colores (hasta 2 opciones y 60 combinaciones); variantes cambia stock, precio o código de cada combinación, por id o por valores.| GET | /api/v1/tienda | Nombre, dirección, país, moneda, si está abierta, si hay retiro, formas de pago y envíos. |
| GET | /api/v1/envios | Los envíos de la tienda, con su id y precio. |
| GET | /api/v1/categorias | Las categorías, con cuántos productos tiene cada una. |
El dueño pone la dirección de su sistema en Conexiones → Otro sistema → Avisos al instante (tiene que ser https://). Desde ahí, cada vez que pasa algo, la tienda manda un POST con JSON:
| Evento | Cuándo |
|---|---|
pedido.nuevo | Entra un pedido (de la tienda, del Encargado, de la API). |
pedido.pagado | Se pagó (Mercado Pago, Stripe, o el dueño lo marcó). |
pedido.estado | Cambió de estado (trae antes). |
pedido.envio | Le pusieron el código de envío. |
pedido.corregido | Se corrigió el cliente, la entrega o lo que lleva (desde la app o la API). |
pedido.plata_devuelta | Se le devolvió plata al comprador (el pedido trae devuelto). |
pedido.aviso | Le salió un mail (o un aviso al celular) al comprador: trae avisos, cuáles (["listo"], ["retiro15"]). Sirve para mandarle una copia por otro canal en el mismo momento. |
producto.cambiado | Cambió el precio, el stock, si se ve, el nombre o el código, o se borró. |
prueba | El dueño tocó «Mandar un aviso de prueba». |
POST https://mi-sistema.com/avisos
X-Cubobase-Evento: pedido.pagado
X-Cubobase-Id: 8123
X-Cubobase-Firma: sha256=5d41402abc4b2a76b9719d911017c592…
{"id": 8123, "evento": "pedido.pagado", "creado": "2026-10-10T19:13:05-03:00",
"tienda": "malva", "pedido": { …como en GET /api/v1/pedidos/{numero}… }}
id sirve para no procesarlo de nuevo.# Python
import hashlib, hmac
def es_de_cubobase(cuerpo: bytes, firma: str, secreto: str) -> bool:
esperada = "sha256=" + hmac.new(secreto.encode(), cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, firma)
Un sistema con varias partes que consultan a cada rato (el estado, los avisos, las ventas, un buscador) se pasa enseguida de las 120 consultas por minuto. Lo que funciona —lo aprendió el primer sistema que se conectó, la gestión de un comercio que le pedía a su tienda vieja unos 1.000 pedidos por minuto en los picos— es una copia propia:
GET /api/v1/pedidos (de a 200) y se mantiene al día con los avisos al instante: cada aviso trae el pedido entero, como está ahora.GET /api/v1/pedidos?modificado_desde=… con la fecha del último que se tiene.actualizado más nuevo; un aviso repetido (el mismo id) se guarda una vez.429, conviene esperar unos segundos y reintentar.Siempre en JSON: {"error": "codigo", "mensaje": "qué pasó, en castellano"}.
| Estado | Código | Qué pasó |
|---|---|---|
| 400 | json_invalido, dato_invalido | Falta algo o está mal escrito. |
| 401 | sin_llave, llave_invalida | Falta la llave, o fue revocada. |
| 403 | sin_profesional, tienda_inactiva | La tienda no tiene la profesional, o no está activa. |
| 404 | no_existe | Ese pedido, producto o dirección no existe. |
| 409 | no_se_puede | El estado del pedido no permite ese cambio. |
| 422 | pedido_invalido, producto_no_existe, variante_no_existe, foto_invalida, direccion_invalida, no_se_puede_devolver | Los datos están bien escritos pero no se puede hacer (sin stock, tienda cerrada…). |
| 429 | demasiados | Más de 120 pedidos por minuto. |
Además de la API, las tiendas tienen un servidor MCP (Model Context Protocol, el estándar con el que los agentes de IA se conectan a otros sistemas). El dueño conecta su Claude, su ChatGPT o cualquier agente compatible, y el agente maneja la tienda con las mismas reglas que la app. Es para todas las tiendas, también la Base.
https://app.cubobase.com/mcp
POST lleva un mensaje JSON-RPC (o una lista) y la respuesta vuelve en JSON. Versiones del protocolo: 2025-11-25, 2025-06-18 y 2025-03-26.S256) y registro dinámico de clientes (RFC 7591). Sin llave, /mcp contesta 401 con WWW-Authenticate: Bearer resource_metadata="…"; los datos están en /.well-known/oauth-protected-resource (RFC 9728) y /.well-known/oauth-authorization-server (RFC 8414). El dueño entra a su cuenta y toca «Autorizar»; el agente recibe una llave cb_… que el dueño desconecta cuando quiere en Conexiones → Tu agente de IA./mcp con Authorization: Bearer cb_… de «Otro sistema» (en ese caso, solo con la tienda profesional).ver_tienda, abrir_o_cerrar, ventas, mas_vendido, quien_debe, buscar_pedidos, ver_pedido, crear_pedido, cambiar_pedido, buscar_productos, ver_producto, crear_producto, cambiar_producto, borrar_producto, ver_categorias y ver_envios; las fotos que el dueño tiene en el celular o la compu: link_para_subir_fotos (un link de un solo uso para subirlas, porque las que se adjuntan en el chat no le llegan a la herramienta) y ver_fotos_subidas (cada una con su nombre, para usarla en un producto o en el diseño, y una foto chica como contenido image); con reservas, reservas_del_dia; con la profesional, mensajes_de_la_pagina. Y el Diseño de la tienda: ver_diseno, cambiar_diseno (cambia el borrador; la tienda no cambia hasta publicar), subir_foto_diseno (por link https), ver_como_queda (dos fotos del borrador, en la compu y en el celular, como contenido image, y un link de vista previa) y publicar_diseno (sin confirmar dice qué cambia y no publica; en la Base no publica). Cada una dice si solo lee (readOnlyHint) o si borra (destructiveHint). Por dentro usan esta misma API: mismas validaciones, mismos avisos al comprador y el mismo límite de 120 por minuto.Para probarlo desde la terminal con Claude Code (después, /mcp abre el «Autorizar»):
claude mcp add --transport http cubobase https://app.cubobase.com/mcp
Lo nuevo se agrega sin romper lo que ya anda: los campos que hay no cambian de nombre ni de significado. ¿Dudas? Escribinos a contacto@cubobase.com.