REST · JSON · UTF-8
API de TiendaCloud
Conecta tu inventario con tu sitio web, ERP o cualquier sistema externo. Endpoints REST con autenticación por API key, paginación con cursor y límites de uso por llave. Sin SDKs propietarios — cualquier cliente HTTP estándar funciona.
Base URL
tiendacloud.app
Versión
v1
Cuota default
120 req/min
Autenticación
Bearer Token o x-api-key
Cada solicitud necesita una API key. Pasa la llave por uno de estos dos headers — el primero es el estándar moderno, el segundo existe por compatibilidad con clientes legacy.
# Recomendado
Authorization: Bearer tcsk_live_…
# Alternativa
x-api-key: tcsk_live_…Llaves de producción vs. de pruebas
Cuando creas una API key desde la app, eliges su modo. El prefijo identifica visualmente para qué sirve:
tcsk_live_…Para uso en producción. Conecta a tus datos reales y cuenta para tu cuota normal.tcsk_test_…Para integraciones en desarrollo. Mismos datos, marcador para que tu equipo distinga.
Solo el propietario de la tienda puede crear y revocar llaves. Cada llave se muestra una sola vez en su creación — guárdala en un gestor de secretos. Si la pierdes, revócala y crea una nueva.
Permisos (scopes)
Por defecto las llaves son de solo lectura del catálogo público (id, nombre, SKU, precio de venta, stock, etc.). Algunos campos sensibles están detrás de scopes explícitos que se eligen al crear la llave:
| Scope | Habilita | Cuándo usarlo |
|---|---|---|
| cost:read | Incluye cost_price en el producto y en cada variante. | Reportes de margen internos, ERPs. No para storefronts públicos. |
Sin scope, los campos de costo simplemente no aparecen en la respuesta (no son null — el campo está omitido por completo). El arreglo meta.scopes en cada respuesta refleja lo que tu llave puede ver.
Cuotas
Rate limits y headers
La cuota por defecto es 120 solicitudes por minuto por llave. Cada respuesta incluye estos headers para que tu cliente pueda auto-regular:
| Header | Significado |
|---|---|
| X-RateLimit-Limit | Máximo permitido por minuto. |
| X-RateLimit-Remaining | Solicitudes restantes en la ventana actual. |
| X-RateLimit-Reset | UNIX timestamp en segundos cuando la ventana se reinicia. |
| Retry-After | Solo en 429. Segundos a esperar antes de reintentar. |
Si necesitas un cupo mayor, escríbenos: soporte@tiendacloud.app.
Errores
Envoltura consistente
Todos los errores devuelven una estructura predecible. El campo code es un enum estable que puedes usar para ramificar lógica; el request_id te sirve para abrir un caso de soporte si algo falla de nuestro lado.
{
"error": "Invalid or revoked API key.",
"code": "invalid_api_key",
"request_id": "req_8K2pX9aB4cD6fH1jL"
}Códigos de error
| code | HTTP | Causa |
|---|---|---|
| missing_api_key | 401 | No se envió ningún header de autenticación. |
| invalid_api_key | 401 | El API key no existe o fue revocado. |
| invalid_limit | 400 | El parámetro limit no es un número entre 1 y 500. |
| invalid_cursor | 400 | El cursor no es válido. Pasa el next_cursor exacto que devolvió la respuesta anterior; trátalo como una cadena opaca. |
| invalid_updated_after | 400 | updated_after no es un timestamp ISO 8601 válido. |
| invalid_product_id | 400 | El id en /inventory/{id} no es un UUID. |
| not_found | 404 | El producto no existe o no pertenece a tu tienda. |
| method_not_allowed | 405 | Método HTTP no soportado. Mira el header Allow para los métodos permitidos. |
| rate_limited | 429 | Excediste el cupo. Incluye Retry-After: 60 segundos. |
| internal_error | 500 | Algo falló de nuestro lado. Reintenta o contáctanos con el request_id. |
Endpoints
GET /api/public/v1/inventory
Devuelve los productos activos de tu tienda con el stock sumado entre todas tus sucursales.
Query params
| Parámetro | Tipo | Descripción |
|---|---|---|
| limit | integer · opcional | Productos por página. Entre 1 y 500. Por defecto 100. |
| cursor | string · opcional | El next_cursor opaco de la respuesta anterior. Trátalo como una cadena negra — no lo modifiques. Omítelo en la primera llamada. |
| updated_after | ISO 8601 · opcional | Solo productos modificados después de este timestamp. Útil para sincronización incremental. |
| category | string · opcional | Filtro exacto por categoría. |
| in_stock | boolean · opcional | Cuando es true omite productos con cantidad 0. |
Solicitud
curl -X GET 'https://tiendacloud.app/api/public/v1/inventory?limit=100' \
-H 'Authorization: Bearer tcsk_live_…' \
-H 'Accept: application/json'const response = await fetch(
'https://tiendacloud.app/api/public/v1/inventory?limit=100',
{
headers: {
'Authorization': `Bearer ${process.env.TIENDACLOUD_API_KEY}`,
'Accept': 'application/json',
},
},
)
if (!response.ok) {
const { error, code, request_id } = await response.json()
throw new Error(`[${code}] ${error} (${request_id})`)
}
const { products, pagination } = await response.json()
console.log(`${products.length} productos · ${pagination.has_more ? 'hay más' : 'fin'}`)import os, requests
resp = requests.get(
"https://tiendacloud.app/api/public/v1/inventory",
headers={"Authorization": f"Bearer {os.environ['TIENDACLOUD_API_KEY']}"},
params={"limit": 100},
)
resp.raise_for_status()
data = resp.json()
print(f"{len(data['products'])} productos")Respuesta
{
"products": [
{
"id": "0c8f2e6a-1234-5678-90ab-cdef01234567",
"name": "Cargador 25W",
"sku": "CGR-25W-01",
"price": 950,
"category": "Cargadores",
"is_taxable": true,
"image_url": "https://cdn.tiendacloud.app/products/cgr-25w-01.jpg",
"sale_mode": "unit",
"bulk_unit": null,
"bulk_presets": [],
"alerts_enabled": true,
"warn_threshold": 20,
"low_threshold": 5,
"variant_count": 0,
"quantity": 12,
"variants": [],
"locations": [
{ "location_id": "7f1e3a8b-…", "location_name": "Principal", "quantity": 12 }
],
"created_at": "2026-05-12T10:34:00.000Z",
"updated_at": "2026-06-20T16:51:00.000Z"
}
],
"pagination": {
"limit": 100,
"next_cursor": null,
"has_more": false
},
"meta": {
"max_updated_at": "2026-06-20T16:51:00.000Z",
"filters_applied": {
"updated_after": null,
"category": null,
"in_stock": false
}
}
}Los precios están en pesos dominicanos (RD$) como números — sin separadores de miles ni símbolo de moneda.
Filtros
Acota la respuesta
Cada producto siempre regresa con sus variantes (tallas, colores) y stock por sucursal — la misma forma que /inventory/{id} devuelve. No hay flag include porque la forma es estable. Si no necesitas un producto, fíltralo:
# Filtra por categoría y solo productos con stock
GET /api/public/v1/inventory?category=Cargadores&in_stock=true
# Combinable con paginación e incremental sync
GET /api/public/v1/inventory?category=Vestidos&limit=50&updated_after=2026-06-22T00:00:00ZCuando un producto no tiene variantes reales (tallas/colores), el arreglo variants llega vacío — el campo variant_count sigue siendo el chequeo barato para decidir si recorrerlas.
Endpoints
GET /api/public/v1/inventory/{id}
Detalle completo de un solo producto. Siempre incluye sus variantes y stock por sucursal — el caller ya pidió ver todo. Comparte la cuota de rate-limit con el endpoint de listado.
curl -X GET 'https://tiendacloud.app/api/public/v1/inventory/0c8f2e6a-1234-5678-90ab-cdef01234567' \
-H 'Authorization: Bearer tcsk_live_…' \
-H 'Accept: application/json'{
"product": {
"id": "0c8f2e6a-1234-5678-90ab-cdef01234567",
"name": "Vestido Floral Verano",
"sku": "VST-FLR-VRN",
"price": 1850,
"category": "Vestidos",
"is_taxable": true,
"image_url": "https://cdn.tiendacloud.app/products/vst-flr-vrn.jpg",
"sale_mode": "unit",
"bulk_unit": null,
"bulk_presets": [],
"alerts_enabled": true,
"warn_threshold": 10,
"low_threshold": 3,
"variant_count": 6,
"quantity": 14,
"variants": [
{
"id": "...",
"sku": "VST-FLR-VRN-M-CRL",
"attribute_1_value": "M",
"attribute_2_value": "Coral",
"price": null,
"image_url": null,
"is_default": true,
"quantity": 5
}
],
"locations": [
{ "location_id": "...", "location_name": "Principal", "quantity": 10 },
{ "location_id": "...", "location_name": "Sucursal 2", "quantity": 4 }
],
"created_at": "2026-05-12T10:34:00.000Z",
"updated_at": "2026-06-20T16:51:00.000Z"
}
}Devuelve 404 not_found si el producto no existe o no pertenece a tu tienda — sin distinguir entre los dos casos (no revelamos existencia entre tiendas).
Patrones
Sincronización incremental
Para mantener tu sitio web o ERP al día, no descargues todo el catálogo en cada corrida. Guarda el meta.max_updated_at que viene en cada respuesta y pásalo como updated_after en la siguiente.
// Carga inicial: trae todo
let cursor = null
let lastSeen = null
do {
const url = new URL('https://tiendacloud.app/api/public/v1/inventory')
url.searchParams.set('limit', '500')
if (cursor) url.searchParams.set('cursor', cursor)
const { products, pagination, meta } = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json())
for (const p of products) save(p)
cursor = pagination.next_cursor
if (meta.max_updated_at) lastSeen = meta.max_updated_at
} while (cursor)
// Persiste `lastSeen` localmente.
// Sincronización subsiguiente (cron cada 5 min, p. ej.)
const url = new URL('https://tiendacloud.app/api/public/v1/inventory')
url.searchParams.set('updated_after', lastSeen)
const { products, meta } = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json())
for (const p of products) upsert(p)
if (meta.max_updated_at) lastSeen = meta.max_updated_atPaginación
Cursor-based
Cuando hay más resultados, la respuesta incluye pagination.next_cursor y has_more: true. Páralo cuando has_more sea false.
let cursor = null
do {
const url = new URL('https://tiendacloud.app/api/public/v1/inventory')
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } })
const { products, pagination } = await res.json()
for (const p of products) handle(p)
cursor = pagination.next_cursor
} while (cursor)Caching
ETag + If-None-Match
Cada respuesta incluye un header ETag derivado del estado de la página. Vuelve a enviarlo en If-None-Match y si nada cambió recibirás 304 Not Modified sin cuerpo de respuesta — más rápido, menos ancho de banda. La llamada sigue contando para tu cuota.
# Primera llamada — guarda el ETag
curl -i 'https://tiendacloud.app/api/public/v1/inventory?limit=100' \
-H 'Authorization: Bearer tcsk_live_…'
# ← ETag: "a1b2c3d4e5..."
# Próxima llamada — pasa el ETag
curl -i 'https://tiendacloud.app/api/public/v1/inventory?limit=100' \
-H 'Authorization: Bearer tcsk_live_…' \
-H 'If-None-Match: "a1b2c3d4e5..."'
# ← 304 Not Modified (sin cuerpo) si nada cambió.Especificación
OpenAPI 3.1
La descripción formal de la API está disponible en /api/openapi.json. Impórtala en Postman, Insomnia, o úsala con generadores de SDKs (openapi-generator, oazapfts, fern, etc.) para que tus clientes salgan tipados automáticamente.
Soporte
Contacto
¿Algo no funciona como esperas? Escribe a soporte@tiendacloud.app e incluye el request_id que devolvimos. Eso nos permite encontrar exactamente la solicitud que te dio problema en nuestros logs.