Integration API v1
API push inbound: tu sistema envía catálogo y precios a PriceTag vía HTTP. PriceTag no descarga datos del ERP y no escribe hacia él en v1.
Pensada para equipos técnicos de ERP, POS, middleware y system integrator. Contrato machine-readable: OpenAPI 3 · Versión 1.0 · Actualizada 2026-09-23.
Qué hace (y qué no)
Sección titulada «Qué hace (y qué no)»PriceTag es la capa de presentación en góndola (ESL + app de tienda). El ERP / POS suele seguir siendo la source of truth comercial; PriceTag mantiene el snapshot operativo para mostrar y publicar los precios.
Hace
- Upsert de catálogo y actualizaciones de precio por EAN
- Auth con token de integración ligado a una tienda
- Resultado parcial por batch (
accepted/rejected[]) - Staging de precios en review in-app (por defecto)
No hace (v1)
- Sync bidireccional o webhook hacia el ERP
- Exponer el snapshot completo o las ops internas
- Conectores vendor-specific (SAP, Danea, Softcare, …)
- Auto-publish de hardware / Base Station en el mismo request
Un solo contrato HTTP: quién empuja (ERP, middleware, script, Google Apps Script) es irrelevante.
Prerrequisitos
Sección titulada «Prerrequisitos»- Cuenta PriceTag con acceso a la tienda target.
storeId— UUID de la tienda (en la app o facilitado en el onboarding).- Token de integración — en la app: Ajustes → Conexiones → Create Token.
- El plaintext aparece solo al crearlo o rotarlo: guárdalo de inmediato en un secret store.
- La revocación invalida el token al instante (kill switch).
- Capacidad de hacer
HTTPSPOST/GETcon headerAuthorizationy body JSON.
Base URL
Sección titulada «Base URL»| Entorno | Base URL |
|---|---|
| Production | https://webapp-pricetag-production.up.railway.app/api |
Prefijo de recursos:
{BASE}/v1/integration/stores/{storeId}Ejemplo:
https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/19c75782-90ed-4821-85d7-50018ec0beb9/healthEn los POST usad Content-Type: application/json.
Autenticación
Sección titulada «Autenticación»Usad uno de estos headers:
Authorization: Bearer <integration_token>X-PriceTag-Key: <integration_token>| Situación | Respuesta |
|---|---|
| Scope | Solo el storeId ligado al token |
| JWT de usuario de la app | No aceptado |
| Token de otro store | 403 |
| Token ausente, inválido o revocado | 401 |
El flag UI “enabled” del token no bloquea las llamadas: el control operativo es la revocación.
Conceptos clave
Sección titulada «Conceptos clave»Clave de producto = EAN
Sección titulada «Clave de producto = EAN»Todos los productos partner se direccionan por EAN (string, trim). Los ID internos de PriceTag no forman parte del contrato.
Campos JSON en inglés
Sección titulada «Campos JSON en inglés»| Campo | Significado |
|---|---|
ean |
Código de barras / EAN |
name |
Nombre del producto (catalog) |
price |
Precio |
department |
Departamento (opcional, catalog) |
sku |
Código interno / SKU (opcional) |
brand no es obligatorio (legacy aceptado si se envía). No enviéis nome / prezzo (claves italianas): el body se rechaza.
Formato de precio
Sección titulada «Formato de precio»| Dirección | Formato |
|---|---|
Entrada (POST) |
Decimal con punto, string o number — p. ej. "3.49" o 3.49 |
Salida (GET producto) |
Display italiano — p. ej. "3,49 €" |
PriceTag aplica las mismas reglas de normalización del import de precios in-app.
Éxito parcial
Sección titulada «Éxito parcial»Los POST batch suelen responder HTTP 200 aunque fallen algunas filas. Comprobad siempre:
accepted/created/updatedrejected[]con{ ean, code, message }
Body no válido → 400. Operaciones no aplicables al snapshot → 422.
Idempotencia
Sección titulada «Idempotencia»Header opcional:
Idempotency-Key: <string ≤ 128 caracteres>| Caso | Comportamiento |
|---|---|
| Misma clave + mismo body | Misma respuesta memorizada (TTL 24 h); sin doble aplicación |
| Misma clave + body distinto | 409 (idempotency_mismatch) |
| Clave ausente | Cada request es independiente |
Usad el ID del job / batch de vuestro middleware.
Flujo de precios y review
Sección titulada «Flujo de precios y review»Por defecto los cambios de precio no actualizan de inmediato la góndola: entran en review en la app. El operador debe Confirmar o Descartar (el modal no se cierra con clic fuera / Escape).
- El partner envía un
POSTde catálogo o precios. - La API responde
200constatus: "pending_review"yreviewId(si hay diff de precio). - En la app: modal Nuevos precios → Confirmar o Descartar.
- Tras Confirmar: Enviar a la cola para publicar en las ESL (si están asignadas).
Policy de review
Sección titulada «Policy de review»Usada en POST .../prices.
| Valor | Efecto |
|---|---|
require_review |
Default. Staging pending; sin cambio en shelf hasta Confirmar. Respuesta: status: "pending_review", reviewId. |
apply_immediate |
Opt-out: last-write en el snapshot sin modal. |
Catalog y precios
Sección titulada «Catalog y precios»En POST .../catalog:
| Situación | Efecto |
|---|---|
| EAN nuevo | Upsert con price aplicado de inmediato |
EAN existente, price igual al shelf |
Actualiza ficha (name, sku, department, …) |
EAN existente, price distinto |
Ficha actualizada; precio shelf sin cambios; staging review + pending_review |
Cola ESL
Sección titulada «Cola ESL»Relevante en POST .../prices solo con apply_immediate (queuePolicy).
| Valor | Efecto |
|---|---|
snapshot_only |
Default. Solo snapshot (sin cola ESL automática) |
enqueue_if_esl |
Encola si el producto tiene una etiqueta ESL asignada |
enqueue_always |
Encola siempre |
Con require_review, tras Confirmar el operador usa Enviar a la cola en la app (como una modificación manual). No hay auto-publish de hardware en el request de integración.
Endpoints
Sección titulada «Endpoints»Path absoluto = Base URL + path debajo.
| Método | Path | Descripción |
|---|---|---|
GET |
/v1/integration/stores/{storeId}/health |
Token válido + tienda alcanzable |
GET |
/v1/integration/stores/{storeId}/products/{ean} |
Lectura de producto por EAN |
POST |
/v1/integration/stores/{storeId}/catalog |
Upsert de catálogo (batch) |
POST |
/v1/integration/stores/{storeId}/prices |
Actualización de precios (batch) |
Verifica credenciales y storeId.
Respuesta 200:
{ "ok": true, "storeId": "19c75782-90ed-4821-85d7-50018ec0beb9", "enabled": true}enabled refleja el estado UI; no bloquea las mutaciones.
Lectura de producto
Sección titulada «Lectura de producto»Lectura puntual (debug / reconciliación).
Respuesta 200:
{ "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3,49 €", "department": "Ortofrutta", "sku": "ART-9912"}404 si el EAN no existe en la tienda.
Catalog (upsert)
Sección titulada «Catalog (upsert)»Upsert por EAN. En v1 el único mode soportado es "upsert".
Request:
{ "items": [ { "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3.49", "department": "ORTOFRUTTA", "sku": "ART-9912" } ], "mode": "upsert"}| Campo | Obligatorio | Notas |
|---|---|---|
ean |
sí | No vacío |
name |
sí | No vacío tras trim |
price |
sí | Ver formato de precio |
department |
no | Label de departamento |
sku |
no | Mapeado a código interno |
Header recomendado: Idempotency-Key.
Respuesta 200: ver Respuesta batch. Con diff de precio en EAN existentes: status: "pending_review" + reviewId.
Actualiza solo los precios de productos ya presentes (por EAN).
Request:
{ "items": [ { "ean": "8051277182755", "price": "3.29" } ], "queuePolicy": "snapshot_only", "reviewPolicy": "require_review"}| Campo | Default | Notas |
|---|---|---|
items[].ean |
— | Obligatorio |
items[].price |
— | Obligatorio |
queuePolicy |
snapshot_only |
Sobre todo con apply_immediate |
reviewPolicy |
require_review |
Staging in-app |
EAN desconocido → fila en rejected (product_not_found); no falla el batch entero.
Respuesta batch
Sección titulada «Respuesta batch»Esquema común a los POST catalog y prices:
{ "accepted": 1, "created": 0, "updated": 1, "status": "pending_review", "reviewId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "rejected": [ { "ean": "9999999999999", "code": "product_not_found", "message": "Prodotto non trovato per EAN" } ]}| Campo | Significado |
|---|---|
accepted |
Filas aceptadas / mergeadas |
created |
Productos nuevos (típicamente catalog) |
updated |
Productos actualizados |
status |
pending_review o applied |
reviewId |
UUID de la review (si pending_review) |
rejected[] |
Filas no aplicadas, con código máquina |
PriceTag no devuelve el snapshot completo en la respuesta.
Códigos HTTP
Sección titulada «Códigos HTTP»| HTTP | Cuándo |
|---|---|
200 |
Batch procesado (también con rejected no vacío) |
400 |
Body no válido / schema |
401 |
Token ausente o inválido |
403 |
Token de otro store |
404 |
Store o producto (GET) no encontrado |
409 |
Idempotency-Key reutilizada con body distinto |
422 |
Ops no aplicables al estado actual |
429 |
Rate limit superado |
Códigos rejected
Sección titulada «Códigos rejected»Códigos típicos en rejected[].code:
| Code | Contexto |
|---|---|
invalid_ean |
EAN ausente / vacío |
invalid_name |
name ausente (catalog) |
invalid_price |
price no parseable |
product_not_found |
EAN ausente en el store (/prices) |
unchanged_price |
Precio ya igual al shelf (/prices + review) |
skipped |
Filas saltadas por el merge catalog |
Límites
Sección titulada «Límites»| Límite | Valor |
|---|---|
Items por POST (catalog / prices) |
máx. 1.000 |
| Rate limit | 60 solicitudes / minuto por token |
| Productos totales por tienda | máx. 5.000 |
Idempotency-Key |
máx. 128 caracteres; TTL de respuesta 24 h |
Más allá de los límites: 400 (batch demasiado grande) o 429 (rate).
Ejemplos curl
Sección titulada «Ejemplos curl»Sustituid STORE_ID y TOKEN.
curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/health"Catalog (upsert)
Sección titulada «Catalog (upsert)»curl -sS -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: catalog-job-20260923-001" \ -d '{ "items": [ { "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3.49", "department": "ORTOFRUTTA", "sku": "ART-9912" } ], "mode": "upsert" }' \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/catalog"Prices (review por defecto)
Sección titulada «Prices (review por defecto)»curl -sS -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: prices-job-20260923-001" \ -d '{ "items": [ { "ean": "8051277182755", "price": "3.29" } ], "queuePolicy": "snapshot_only", "reviewPolicy": "require_review" }' \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/prices"Respuesta típica: "status": "pending_review", "reviewId": "...". En la app aparece el modal Nuevos precios.
Lectura de producto
Sección titulada «Lectura de producto»curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/products/8051277182755"Checklist de integración
Sección titulada «Checklist de integración»- Obtener
storeIdy crear el token en Conexiones. - Guardar el token en un vault; no lo commitéis.
GET .../health→ok: true.- Push de pocos EAN de prueba (catalog o prices) con
Idempotency-Key. - Verificar
accepted/rejectedy, si cambiaron precios,pending_review. - En la app: modal Nuevos precios → Confirmar o Descartar.
- Tras Confirmar: Enviar a la cola (si hay ESL) para publicar en góndola.
- En automatización: batch ≤ 1000, máx. 60 req/min, reintentar solo con la misma
Idempotency-Keya igualdad de body.
Fuera de alcance v1
Sección titulada «Fuera de alcance v1»- CRUD de etiquetas ESL, plano de tienda, departamentos como recurso dedicado
- Publish / ACK de Base Station o BLE en el mismo request
replace_alldel catálogo- Webhook outbound PriceTag → ERP
- Escritura de precios hacia el ERP
- API de billing / multi-token con permisos granulares (roadmap)
- Variantes de contrato por vendor ERP
OpenAPI y soporte
Sección titulada «OpenAPI y soporte»- Contrato OpenAPI:
/integration-api-v1.openapi.yaml - Onboarding / token de prueba: contactar a PriceTag (
storeId+ guía Conexiones) - Incidentes de producción: indicar
storeId, timestamp UTC,Idempotency-Keyy body (sin token en claro)