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.

bash
# 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:

ScopeHabilitaCuándo usarlo
cost:readIncluye 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:

HeaderSignificado
X-RateLimit-LimitMáximo permitido por minuto.
X-RateLimit-RemainingSolicitudes restantes en la ventana actual.
X-RateLimit-ResetUNIX timestamp en segundos cuando la ventana se reinicia.
Retry-AfterSolo 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.

json
{
  "error":      "Invalid or revoked API key.",
  "code":       "invalid_api_key",
  "request_id": "req_8K2pX9aB4cD6fH1jL"
}

Códigos de error

codeHTTPCausa
missing_api_key401No se envió ningún header de autenticación.
invalid_api_key401El API key no existe o fue revocado.
invalid_limit400El parámetro limit no es un número entre 1 y 500.
invalid_cursor400El cursor no es válido. Pasa el next_cursor exacto que devolvió la respuesta anterior; trátalo como una cadena opaca.
invalid_updated_after400updated_after no es un timestamp ISO 8601 válido.
invalid_product_id400El id en /inventory/{id} no es un UUID.
not_found404El producto no existe o no pertenece a tu tienda.
method_not_allowed405Método HTTP no soportado. Mira el header Allow para los métodos permitidos.
rate_limited429Excediste el cupo. Incluye Retry-After: 60 segundos.
internal_error500Algo 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ámetroTipoDescripción
limitinteger · opcionalProductos por página. Entre 1 y 500. Por defecto 100.
cursorstring · opcionalEl next_cursor opaco de la respuesta anterior. Trátalo como una cadena negra — no lo modifiques. Omítelo en la primera llamada.
updated_afterISO 8601 · opcionalSolo productos modificados después de este timestamp. Útil para sincronización incremental.
categorystring · opcionalFiltro exacto por categoría.
in_stockboolean · opcionalCuando es true omite productos con cantidad 0.

Solicitud

bash
curl -X GET 'https://tiendacloud.app/api/public/v1/inventory?limit=100' \
  -H 'Authorization: Bearer tcsk_live_…' \
  -H 'Accept: application/json'
javascript
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'}`)
python
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

json
{
  "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:

bash
# 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:00Z

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

bash
curl -X GET 'https://tiendacloud.app/api/public/v1/inventory/0c8f2e6a-1234-5678-90ab-cdef01234567' \
  -H 'Authorization: Bearer tcsk_live_…' \
  -H 'Accept: application/json'
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.

javascript
// 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_at

Paginació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.

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

bash
# 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.