Conceptos clave

Entendé los fundamentos de TiendaAPI antes de construir tu integración.

Autenticación

TiendaAPI usa dos métodos de autenticación según la API que estés usando:

API de tienda (Storefront)

Para los endpoints públicos del storefront (productos, carrito, checkout), pasá la API key de tu tienda en el header x-api-key:

curl http://localhost:3000/api/store/products \
  -H "x-api-key: ta_abc123def456..."

La API key identifica a qué tienda pertenece el request. Cada tienda tiene una clave única. Las API keys empiezan con ta_.

API de administración

Para los endpoints de admin (CRUD de productos, gestión de pedidos), primero iniciá sesión para obtener un JWT, luego pasalo en el header Authorization:

# Paso 1: Iniciar sesión
curl -X POST http://localhost:3000/api/admin/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@demo.com", "password": "password123"}'

# Respuesta: { "data": { "token": "eyJhbGciOiJIUzI1NiIs..." } }

# Paso 2: Usar el token
curl http://localhost:3000/api/admin/products \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Los tokens JWT expiran después de 24 horas. Iniciá sesión de nuevo para obtener uno nuevo.

API de plataforma (interna)

Para crear nuevas tiendas, pasá la clave secreta de plataforma en el header x-platform-key. Esta clave se configura mediante la variable de entorno PLATFORM_SECRET_KEY.

Atención: Nunca expongas tu JWT de admin ni la clave de plataforma en código del lado del cliente. La API key de tienda (ta_) es segura para el frontend — solo permite lectura y operaciones de carrito.

Multi-tenancy

TiendaAPI es multi-tenant desde el primer día. Cada tienda está completamente aislada:

  • Tienda A nunca puede ver los productos, pedidos o clientes de la Tienda B
  • Cada tienda tiene su propia API key, credenciales de admin y configuración
  • El aislamiento de datos se aplica en dos niveles: middleware de la aplicación + Row-Level Security (RLS) de PostgreSQL

Cómo funciona

┌─────────────────────────────────────┐
│         Llega un request a la API    │
│   Header: x-api-key: ta_abc123...    │
└──────────┬──────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│   El middleware resuelve el store_id  │
│   desde la API key / JWT / URL slug  │
└──────────┬──────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│   Cada query a la DB incluye         │
│   WHERE store_id = '<id_resuelto>'   │
└──────────┬──────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│   PostgreSQL RLS garantiza el        │
│   aislamiento aunque haya un bug     │
└──────────────────────────────────────┘

Precios y moneda

Todos los precios en TiendaAPI se almacenan y transmiten como enteros en centavos. Esto evita problemas de precisión con números de punto flotante.

Valor en la APIMostrar como
2500$25.00
99$0.99
1000000$10,000.00

Para mostrar precios en tu frontend:

// JavaScript
function formatPrice(cents) {
  return new Intl.NumberFormat('es-AR', {
    style: 'currency',
    currency: 'ARS',
  }).format(cents / 100);
}

formatPrice(2500); // "$25,00"
Atención: Al crear o actualizar productos, siempre enviá el precio en centavos. Enviar 25.00 será rechazado — enviá 2500.

Paginación

Todos los endpoints de listado usan paginación basada en cursor. Es más confiable que la paginación por offset para datos en tiempo real.

Cómo funciona

# Primera página (sin cursor)
GET /api/store/products?limit=20

# La respuesta incluye meta:
{
  "data": [...],
  "meta": {
    "has_more": true,
    "cursor": "550e8400-e29b-41d4-a716-446655440000",
    "count": 20,
    "limit": 20
  }
}

# Siguiente página (pasá el cursor)
GET /api/store/products?limit=20&cursor=550e8400-e29b-41d4-a716-446655440000

Parámetros

ParámetroTipoDefaultDescripción
limitentero20Ítems por página (1-100)
cursorstringCursor de la respuesta anterior

Meta de la respuesta

CampoTipoDescripción
has_morebooleanSi hay más ítems disponibles
cursorstring | nullPasá a ?cursor= para la siguiente página
countenteroÍtems devueltos en esta página
limitenteroMáximo de ítems por página

Manejo de errores

Todos los errores siguen el formato estructurado RFC 9457. Cada respuesta de error incluye un code legible por máquinas y un message legible por humanos.

{
  "status": 422,
  "code": "VALIDATION_ERROR",
  "message": "La validación del request falló",
  "details": {
    "errors": {
      "price": ["El precio debe ser al menos 1 centavo"],
      "slug": ["El slug solo puede contener letras minúsculas, números y guiones"]
    }
  }
}

Códigos de error comunes

HTTPCódigoSignificado
400BAD_REQUESTRequest inválido (slug duplicado, carrito vacío, etc.)
401UNAUTHORIZEDCredenciales de auth faltantes o inválidas
403FORBIDDENSin permisos / límite del plan superado
404NOT_FOUNDEl recurso no existe (o pertenece a otra tienda)
422VALIDATION_ERROREl cuerpo del request falló la validación
429RATE_LIMITEDDemasiados requests — revisá el header Retry-After
500INTERNAL_ERRORError del servidor (por favor reportalo)

Manejo de errores en código

const res = await fetch('/api/store/products', {
  headers: { 'x-api-key': apiKey }
});

if (!res.ok) {
  const error = await res.json();

  switch (error.code) {
    case 'UNAUTHORIZED':
      // API key inválida o faltante
      break;
    case 'RATE_LIMITED':
      // Esperar y reintentar
      const retryAfter = res.headers.get('Retry-After');
      break;
    case 'VALIDATION_ERROR':
      // Mostrar errores por campo al usuario
      console.log(error.details.errors);
      break;
  }
}

Próximos pasos