Plan gratis · Sin tarjeta · 0 % comisión
Para quien integra

Conectá cualquier sistema con la tienda.

Una API HTTP con JSON y un token: catálogo, stock, pedidos y clientes. Y webhooks firmados, para que tu sistema se entere de un pedido nuevo sin tener que preguntar cada minuto.

Sin SDK: es HTTP y JSONUn token por sistema, revocableWebhooks con firma HMAC
Empezar

Tres minutos, contando el café.

  1. 1La dueña de la tienda entra a Configuración → API, crea un token y elige qué puede hacer. El token se muestra una sola vez.
  2. 2Lo mandás en cada llamada como Authorization: Bearer tapi_…
  3. 3Probá con GET /: te dice de qué tienda es el token y qué permisos tiene.

Lo que conviene saber antes

  • Un token es de UNA tienda. De cuál, lo decide el token: no hay parámetro de tienda en ninguna ruta.
  • Todos los importes van en centavos, en números enteros. $18.900 es 1890000.
  • 120 lecturas y 30 escrituras por minuto por token. Si te pasás, 429 con Retry-After.
  • Las fechas son ISO 8601 con zona. Los ids son opacos: no les busques forma.

Dirección base

https://app.tiendapi.com/api/publica/v1

La primera llamada

curl https://app.tiendapi.com/api/publica/v1 \
  -H "Authorization: Bearer tapi_…"

{
  "version": "v1",
  "tienda": { "id": "org-…", "nombre": "Lolu", "slug": "lolu" },
  "token": { "nombre": "ERP Tango", "permisos": ["productos:leer"] }
}
Paginación

Por página para mirar, por cursor para recorrer.

Todos los listados devuelven datos y paginacion. Con pagina tenés el total y el número de páginas, que es lo cómodo para mostrar. Para recorrer todo usá el cursor: repetí con ?cursor= hasta que venga vacío.

Y no es una preferencia de estilo: con páginas, un catálogo de más de 5.000 filas no se puede recorrer entero, y un recorrido largo saltea o repite filas si la tienda carga productos mientras corre. El cursor no tiene ninguno de los dos problemas.

# Recorrer el catálogo entero
cursor=""
while :; do
  r=$(curl -s "https://app.tiendapi.com/api/publica/v1/productos?porPagina=100&cursor=$cursor" \
        -H "Authorization: Bearer tapi_…")
  echo "$r" | jq -c '.datos[]'
  cursor=$(echo "$r" | jq -r '.paginacion.siguiente')
  [ -z "$cursor" ] && break
done
Recursos

Todo lo que la v1 expone.

GET/Cualquier token

Probar el token

Devuelve de qué tienda es el token y qué permisos tiene. Es lo primero que conviene llamar al conectar: si esto anda, el problema está en otro lado.

Ejemplo

curl https://app.tiendapi.com/api/publica/v1 \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "version": "v1",
  "tienda": { "id": "org-…", "nombre": "Lolu", "slug": "lolu" },
  "token": { "nombre": "ERP Tango", "permisos": ["productos:leer", "stock:escribir"] }
}
GET/productosproductos:leer

Listar productos

El catálogo de la tienda, paginado. Sin las variantes: para eso está la ficha.

  • Por omisión trae sólo los activos. `estado=todos` incluye los archivados, que son los que la tienda dejó de vender pero siguen en pedidos viejos.
  • `precioDesdeCents` es el precio de la variante más barata; el de cada variante está en la ficha.

Parámetros

busquedatexto
Busca en el título, el SKU y el código de barras.
estadoactivo · archivado · todos
Por omisión, activo.
categoriaid
Sólo los de esa categoría.
soloConStocktrue · false
Deja afuera lo que está en cero.
paginanúmero
Desde 1.
porPaginanúmero
Hasta 100. Por omisión 50.
cursortexto
El «siguiente» de la respuesta anterior. Manda sobre «pagina».

Ejemplo

curl "https://app.tiendapi.com/api/publica/v1/productos?busqueda=remera&porPagina=2" \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "datos": [
    {
      "id": "6aa1d2…",
      "titulo": "Remera oversize",
      "slug": "remera-oversize",
      "descripcion": "Algodón peinado, corte amplio.",
      "descripcionCorta": "",
      "marca": "Lolu",
      "categorias": ["cat-remeras"],
      "etiquetas": ["verano"],
      "estado": "activo",
      "moneda": "ARS",
      "precioDesdeCents": 1890000,
      "stockTotal": 42,
      "imagenPortadaUrl": "https://…/portada.jpg",
      "creadoEl": "2026-08-01T12:00:00.000+00:00",
      "actualizadoEl": "2026-09-20T09:14:00.000+00:00"
    }
  ],
  "paginacion": {
    "pagina": 1, "porPagina": 2, "total": 37, "totalPaginas": 19,
    "hayMas": true, "siguiente": "6aa1d2…"
  }
}
GET/productos/{id}productos:leer

Una ficha completa

El producto con TODAS sus variantes y sus imágenes. Es lo que hace falta para sincronizar precios y stock.

  • `llevaStock: false` quiere decir que la tienda no cuenta esa variante: `stock` no significa nada y se puede comprar siempre.

Ejemplo

curl https://app.tiendapi.com/api/publica/v1/productos/6aa1d2… \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "id": "6aa1d2…",
  "titulo": "Remera oversize",
  "variantes": [
    {
      "id": "6aa1d3…", "sku": "REM-M-NEG", "codigoDeBarras": "7791234567890",
      "valores": ["M", "Negro"],
      "precioCents": 1890000, "precioTachadoCents": null, "costoCents": 900000,
      "llevaStock": true, "stock": 12, "stockMinimo": 3,
      "unidadDeVenta": "unidad", "pesoGramos": 180
    }
  ],
  "imagenes": [
    { "url": "https://…/1.jpg", "posicion": 0, "esPortada": true, "alt": "Remera negra" }
  ]
}
GET/stockstock:leer

Listar el stock

Una fila por variante, con su SKU y sus unidades. Es la vista que un ERP recorre para sincronizar existencias.

Parámetros

busquedatexto
Por título, SKU o código de barras.
soloConStocktrue · false
Deja afuera lo que está en cero.
cursortexto
Para recorrer todo sin el techo del offset.

Ejemplo

curl "https://app.tiendapi.com/api/publica/v1/stock?porPagina=100" \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "datos": [
    {
      "varianteId": "6aa1d3…", "productoId": "6aa1d2…",
      "productoTitulo": "Remera oversize",
      "sku": "REM-M-NEG", "codigoDeBarras": "7791234567890",
      "valores": ["M", "Negro"],
      "precioCents": 1890000,
      "llevaStock": true, "stock": 12, "stockMinimo": 3,
      "unidadDeVenta": "unidad",
      "actualizadoEl": "2026-09-20T09:14:00.000+00:00"
    }
  ],
  "paginacion": { "pagina": 1, "porPagina": 100, "total": 214, "hayMas": true, "siguiente": "6aa1d3…" }
}
POST/stock/{varianteId}stock:escribir

Ajustar el stock

Dos modos, y hay que elegir uno: «fijar» escribe el número que el ERP contó; «ajustar» suma o resta un movimiento.

  • «ajustar» usa el incremento atómico de la base: no hay carrera con una venta que entra en el mismo momento, y un delta que dejaría el stock en negativo se rechaza.
  • El movimiento queda en el historial a nombre del token, así que en el panel se lee «API · ERP Tango» y no un ajuste anónimo.

Cuerpo

modo*fijar · ajustar
Obligatorio. Decide qué significa el resto.
stockentero ≥ 0
Sólo con «fijar». Las unidades que hay.
deltaentero ≠ 0
Sólo con «ajustar». Lo que entró (+) o salió (−).
motivotexto
Queda en el historial. «conteo de inventario», «rotura».

Ejemplo

curl -X POST https://app.tiendapi.com/api/publica/v1/stock/6aa1d3… \
  -H "Authorization: Bearer tapi_…" \
  -H "Content-Type: application/json" \
  -d '{"modo":"ajustar","delta":-3,"motivo":"rotura"}'

Respuesta

{ "varianteId": "6aa1d3…", "stock": 9, "llevaStock": true }
GET/pedidospedidos:leer

Listar pedidos

Los pedidos de la tienda, del más nuevo al más viejo. Sin los renglones: son una consulta por pedido, o sea cincuenta por página.

Parámetros

estadoabierto · cumplido · cancelado
El estado del pedido.
estadoDePagopendiente · pagado · reembolsado · fallido
Cómo viene el cobro.
estadoDeEnviosin_enviar · preparando · enviado · entregado · retirado · devuelto
Cómo viene la entrega.
clienteIdid
Sólo los de ese cliente.
desde · hastaISO 8601
Rango por fecha de alta. Ej: 2026-09-01T00:00:00Z
cursortexto
Para recorrer todo.

Ejemplo

curl "https://app.tiendapi.com/api/publica/v1/pedidos?estadoDePago=pagado&desde=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "datos": [
    {
      "id": "6ab44e…", "numero": 1042,
      "estado": "abierto", "estadoDePago": "pagado", "estadoDeEnvio": "sin_enviar",
      "cliente": { "id": "", "nombre": "Ana López", "email": "ana@…", "telefono": "+54…", "documento": "" },
      "subtotalCents": 1890000, "descuentoCents": 0, "envioCents": 250000,
      "recargoCents": 0, "totalCents": 2140000, "moneda": "ARS",
      "canal": "tienda", "medioDePago": "mercadopago", "cuponCodigo": "",
      "creadoEl": "2026-09-22T18:02:00.000+00:00",
      "pagadoEl": "2026-09-22T18:04:00.000+00:00",
      "enviadoEl": null, "entregadoEl": null, "canceladoEl": null
    }
  ],
  "paginacion": { "pagina": 1, "porPagina": 50, "total": 312, "hayMas": true, "siguiente": "6ab44e…" }
}
GET/pedidos/{id}pedidos:leer

Un pedido con sus renglones

El mismo pedido, con el detalle de lo que se vendió.

Ejemplo

curl https://app.tiendapi.com/api/publica/v1/pedidos/6ab44e… \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "id": "6ab44e…", "numero": 1042,
  "renglones": [
    {
      "productoId": "6aa1d2…", "varianteId": "6aa1d3…",
      "titulo": "Remera oversize", "variante": "M / Negro", "sku": "REM-M-NEG",
      "cantidad": 1, "precioUnitarioCents": 1890000, "subtotalCents": 1890000
    }
  ]
}
POST/pedidospedidos:crear

Cargar un pedido

Crea un pedido con los precios de la tienda. Es el mismo camino que usa el panel: aplica la lista del cliente, las promociones y el cupón, y reserva el cupo de preventa.

  • ⚠️ El stock se mueve cuando el pedido se COBRA, no al crearlo, igual que en el panel: un pedido pendiente de pago no es mercadería que salió. Como acá no se cobra, el stock queda quieto hasta que alguien lo marque pagado.
  • La lista de precios sale de la ficha del cliente («clienteId») y no se puede mandar por parámetro: si se pudiera, cualquiera se aplicaría el precio mayorista.
  • No acepta «marcarPagado» —acá no se cobra— ni «medioDePago: cuenta_corriente», porque el cargo en el libro del cliente se anota desde el panel.

Cuerpo

clienteNombre*texto
A nombre de quién va.
items*lista
[{ varianteId, cantidad }]. Al menos uno.
clienteIdid
La ficha del cliente. Es lo que trae su lista de precios.
clienteEmail · clienteTelefono · clienteDocumentotexto
Para el remito y los avisos.
envioobjeto
Calle, número, ciudad, código postal…
cuponCodigotexto
Se valida como en la tienda: si no aplica, el pedido entra sin él.
notaDelCliente · notaInternatexto
Lo que pidió, y lo que anota la tienda.

Ejemplo

curl -X POST https://app.tiendapi.com/api/publica/v1/pedidos \
  -H "Authorization: Bearer tapi_…" \
  -H "Content-Type: application/json" \
  -d '{
    "clienteNombre": "Distribuidora Sur",
    "clienteId": "6aa2f1…",
    "items": [{ "varianteId": "6aa1d3…", "cantidad": 24 }]
  }'

Respuesta

HTTP 201
{
  "pedido": {
    "id": "6ab44e…", "numero": 1043,
    "estado": "abierto", "estadoDePago": "pendiente",
    "totalCents": 27360000, "moneda": "ARS",
    "renglones": [ { "varianteId": "6aa1d3…", "cantidad": 24, "precioUnitarioCents": 1140000 } ]
  }
}
GET/clientesclientes:leer

Listar clientes

Las fichas de quienes compran, ordenadas por fecha de alta —que es el orden estable para sincronizar—.

  • Por omisión no trae los archivados: si los trajera, el ERP volvería a dar de alta a quien la tienda dio de baja.

Parámetros

busquedatexto
Por nombre, correo o teléfono.
estadoactivos · archivados · todos
Por omisión, activos.
cursortexto
Para recorrer todo.

Ejemplo

curl "https://app.tiendapi.com/api/publica/v1/clientes?busqueda=distribuidora" \
  -H "Authorization: Bearer tapi_…"

Respuesta

{
  "datos": [
    {
      "id": "6aa2f1…", "nombre": "Distribuidora Sur",
      "email": "compras@…", "telefono": "+54 9 11 …",
      "documentoTipo": "CUIT", "documentoNumero": "30-12345678-9",
      "estado": "aprobado",
      "domicilio": { "calle": "Av. Mitre", "numero": "1200", "ciudad": "Avellaneda", "provincia": "Buenos Aires", "codigoPostal": "1870", "pais": "AR" },
      "creadoEl": "2026-07-14T10:00:00.000+00:00",
      "actualizadoEl": "2026-09-01T16:20:00.000+00:00"
    }
  ],
  "paginacion": { "pagina": 1, "porPagina": 50, "total": 88, "hayMas": true, "siguiente": "6aa2f1…" }
}
POST/clientesclientes:escribir

Dar de alta un cliente

Crea la ficha con los mismos campos y las mismas validaciones que el panel.

  • «tablaDePreciosId» se comprueba contra las listas de ESTA tienda: un id ajeno se descarta en vez de guardarse.
  • La ficha queda a nombre del token, no de una persona: en el panel se ve quién la cargó.
  • No acepta «assignedAgentId» —repartir la cartera es decisión de la tienda— ni el estado: archivar se hace desde el panel.

Cuerpo

nombre*texto
Al menos dos caracteres.
email · telefonotexto
El correo se valida si viene.
documentoTipo · documentoNumerotexto
«CUIT», «30-12345678-9».
cumpleMM-DD
Para el saludo automático. El año no hace falta.
tablaDePreciosIdid
La lista con la que compra.
etiquetaslista de textos
Hasta 20.
domicilioobjeto
calle, numero, piso, codigoPostal, barrio, ciudad, provincia, pais, referencia. Se escribe entero.

Ejemplo

curl -X POST https://app.tiendapi.com/api/publica/v1/clientes \
  -H "Authorization: Bearer tapi_…" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Distribuidora Sur",
    "email": "compras@distribuidorasur.com",
    "documentoTipo": "CUIT",
    "documentoNumero": "30-12345678-9",
    "tablaDePreciosId": "tab-mayorista"
  }'

Respuesta

HTTP 201
{ "cliente": { "id": "cnt-…", "nombre": "Distribuidora Sur", "…": "…" } }
POST/clientes/{id}clientes:escribir

Corregir una ficha

Cambia sólo lo que mandás. Lo que no mandás queda como está.

  • Es POST y no PATCH a propósito: es el método que cualquier cliente HTTP manda sin pensarlo.
  • El domicilio se escribe ENTERO: si mandás sólo la calle, el resto queda vacío. Es para que no queden medias direcciones que nunca existieron.

Cuerpo

los mismos del altatodos opcionales
Al menos uno.

Ejemplo

curl -X POST https://app.tiendapi.com/api/publica/v1/clientes/cnt-… \
  -H "Authorization: Bearer tapi_…" \
  -H "Content-Type: application/json" \
  -d '{ "telefono": "+54 9 11 4444 5555" }'

Respuesta

{ "cliente": { "id": "cnt-…", "telefono": "+54 9 11 4444 5555", "…": "…" } }
GET/clientes/{id}clientes:leer

Una ficha de cliente

La misma forma que el listado. Sin las notas internas: son la conversación de la tienda con esa persona.

Ejemplo

curl https://app.tiendapi.com/api/publica/v1/clientes/6aa2f1… \
  -H "Authorization: Bearer tapi_…"

Respuesta

{ "id": "6aa2f1…", "nombre": "Distribuidora Sur", "…": "…" }
Webhooks

Que la tienda te avise, en vez de preguntar cada minuto.

La tienda configura a qué URL avisarte y de qué. Cada aviso llega como un POST con JSON y una firma que prueba que salió de esa tienda. Si tu servidor no contesta 2xx, se reintenta seis veces con espera creciente: al minuto, a los 5, a los 15, a la hora, a las 6 y a las 24.

Los eventos

pedido.creado

Apenas entra un pedido, por la tienda, el mostrador o el WhatsApp.

Lleva: El pedido con sus renglones.

pedido.pagado

Cuando el pago queda acreditado.

Lleva: El pedido con sus renglones.

pedido.cumplido

Cuando el pedido se da por terminado.

Lleva: El pedido con sus renglones.

producto.actualizado

Alta, edición o archivado de una ficha.

Lleva: El producto completo, con variantes.

stock.actualizado

Cada movimiento de existencias.

Lleva: La variante, el stock que quedó y el motivo.

El cuerpo

POST https://tu-erp.com/tiendapi
X-Tiendapi-Firma: t=1758657600,v1=<hmac>
X-Tiendapi-Evento: pedido.pagado
X-Tiendapi-Id: evt_6ab449…

{
  "id": "evt_6ab449…",
  "evento": "pedido.pagado",
  "ocurrioEl": "2026-09-23T21:49:24.004Z",
  "tienda": { "id": "org-…", "nombre": "Lolu" },
  "datos": { "pedido": { "…": "…" } }
}

Comprobar la firma

const [t, v1] = firma.split(",").map((p) => p.split("=")[1]);
const esperado = crypto
  .createHmac("sha256", SECRETO)
  .update(t + "." + cuerpoCrudo)   // el cuerpo TAL CUAL llegó
  .digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado))) rechazar();
// Y descartá lo que tenga más de 5 minutos.

El id es el mismo si un aviso se reintenta: guardalo y descartá los repetidos. Contestá rápido —un 200 vacío alcanza— y hacé el trabajo después: si tardás más de cinco segundos, se toma como que no llegó.

Cuando algo falla

Siempre la misma forma.

Un error es un JSON con error.codigo —que es estable y sirve para ramificar— y un mensaje en castellano que no. Cuando el problema es un campo, viene además detalles con cuál y por qué.

{
  "error": {
    "codigo": "peticion_invalida",
    "mensaje": "Revisá los datos que mandaste.",
    "detalles": [
      { "campo": "porPagina", "mensaje": "No puede ser mayor que 100." }
    ]
  }
}
HTTPCódigoQué pasóQué hacer
400peticion_invalidaFalta un campo o tiene un valor que no va.Mirá «detalles»: dice qué campo y por qué. No reintentes igual.
401no_autorizadoEl token no sirve: no existe, venció, lo revocaron, está en pausa, o la tienda limitó desde qué IP se puede usar.Avisale a la tienda: desde su panel ve cuál de los casos es. Acá no se distinguen, a propósito.
403sin_permisoEl token es válido pero no tiene ese permiso.La tienda puede emitir otro con el permiso que falta.
404no_encontradoNo existe, o es de otra tienda.Los dos casos contestan igual: distinguirlos dejaría enumerar ids ajenos.
409peticion_invalidaEl ajuste dejaría el stock en negativo.Volvé a leer el stock y recalculá el delta.
413cuerpo_demasiado_grandeEl cuerpo pasa el tope.Partí la operación en varias.
415tipo_no_soportadoFalta el Content-Type: application/json.Agregá la cabecera.
429demasiadas_peticionesPasaste el tope por minuto.Esperá y reintentá con espera creciente. Mirá la cabecera Retry-After.
500error_internoSe rompió algo de nuestro lado.Reintentá más tarde. El detalle queda en nuestros registros, no en la respuesta.

¿Te falta un endpoint?

La v1 crece con lo que se pide. Escribinos qué necesita el sistema que estás conectando y lo miramos.

Ver qué hace la plataforma