Skip to content

Integration API v1

Inbound push API: your system sends catalog and prices to PriceTag over HTTP. PriceTag does not pull data from the ERP and does not write back to it in v1.

Built for ERP, POS, middleware, and system-integrator technical teams. Machine-readable contract: OpenAPI 3 · Version 1.0 · Updated 2026-09-23.

PriceTag is the shelf presentation layer (ESL + store app). The ERP / POS remains, as a rule, the commercial source of truth; PriceTag holds the operational snapshot used to display and publish prices.

Does

  • Upsert catalog and price updates by EAN
  • Auth with an integration token bound to one store
  • Partial batch outcome (accepted / rejected[])
  • Price staging for in-app review (default)

Does not (v1)

  • Bidirectional sync or webhooks to the ERP
  • Expose the full snapshot or internal ops
  • Vendor-specific connectors (SAP, Danea, Softcare, …)
  • Auto-publish hardware / Base Station in the same request

One HTTP contract: who pushes (ERP, middleware, script, Google Apps Script) does not matter.

  1. PriceTag account with access to the target store.
  2. storeId — store UUID (in-app or provided during onboarding).
  3. Integration token — in the app: Impostazioni → Connessioni → Create Token.
    • The plaintext appears only at creation or rotation: save it immediately in a secret store.
    • Revocation invalidates the token immediately (kill switch).
  4. Ability to make HTTPS POST / GET with an Authorization header and JSON body.
Environment Base URL
Production https://webapp-pricetag-production.up.railway.app/api

Resource prefix:

{BASE}/v1/integration/stores/{storeId}

Example:

https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/19c75782-90ed-4821-85d7-50018ec0beb9/health

On POST requests use Content-Type: application/json.

Use one of these headers:

Authorization: Bearer <integration_token>
X-PriceTag-Key: <integration_token>
Situation Response
Scope Only the storeId bound to the token
App user JWT Not accepted
Token for another store 403
Missing, invalid, or revoked token 401

The UI “enabled” flag on the token does not block calls: the operational control is revocation.

All partner products are addressed by EAN (string, trimmed). Internal PriceTag IDs are not part of the contract.

Field Meaning
ean Barcode / EAN
name Product name (catalog)
price Price
department Department (optional, catalog)
sku Internal code / SKU (optional)

brand is not required (legacy accepted if sent). Do not send nome / prezzo (Italian keys): the body is rejected.

Direction Format
Input (POST) Decimal with a dot, string or number — e.g. "3.49" or 3.49
Output (GET product) Italian display — e.g. "3,49 €"

PriceTag applies the same normalization rules as in-app price import.

Batch POSTs usually respond with HTTP 200 even if some rows fail. Always check:

  • accepted / created / updated
  • rejected[] with { ean, code, message }

Invalid body → 400. Operations not applicable to the snapshot → 422.

Optional header:

Idempotency-Key: <string ≤ 128 characters>
Case Behavior
Same key + same body Same stored response (TTL 24 h); no double apply
Same key + different body 409 (idempotency_mismatch)
Missing key Each request is independent

Use your middleware job / batch ID.

By default, price changes do not update the shelf immediately: they enter review in the app. The operator must Conferma (Confirm) or Scarta (Discard) (the modal does not close on outside click / Escape).

  1. The partner sends a catalog or prices POST.
  2. The API responds 200 with status: "pending_review" and reviewId (if there is a price diff).
  3. In the app: Nuovi prezzi modal → Conferma or Scarta.
  4. After Conferma: Invia alla coda to publish to ESLs (if assigned).

Used on POST .../prices.

Value Effect
require_review Default. Staging pending; no shelf change until Conferma. Response: status: "pending_review", reviewId.
apply_immediate Opt-out: last-write on the snapshot with no modal.

On POST .../catalog:

Situation Effect
New EAN Upsert with price applied immediately
Existing EAN, price equal to shelf Updates master data (name, sku, department, …)
Existing EAN, price different Master data updated; shelf price unchanged; review staging + pending_review

Relevant on POST .../prices only with apply_immediate (queuePolicy).

Value Effect
snapshot_only Default. Snapshot only (no automatic ESL queue)
enqueue_if_esl Enqueue if the product has an assigned ESL label
enqueue_always Always enqueue

With require_review, after Conferma the operator uses Invia alla coda in the app (like a manual edit). There is no hardware auto-publish in the integration request.

Absolute path = Base URL + path below.

Method Path Description
GET /v1/integration/stores/{storeId}/health Valid token + reachable store
GET /v1/integration/stores/{storeId}/products/{ean} Read product by EAN
POST /v1/integration/stores/{storeId}/catalog Catalog upsert (batch)
POST /v1/integration/stores/{storeId}/prices Price update (batch)

Verifies credentials and storeId.

200 response:

{
"ok": true,
"storeId": "19c75782-90ed-4821-85d7-50018ec0beb9",
"enabled": true
}

enabled reflects UI state; it does not block mutations.

Point read (debug / reconciliation).

200 response:

{
"ean": "8051277182755",
"name": "POMODORI SECCHI",
"price": "3,49 €",
"department": "Ortofrutta",
"sku": "ART-9912"
}

404 if the EAN does not exist in the store.

Upsert by EAN. In v1 the only supported mode is "upsert".

Request:

{
"items": [
{
"ean": "8051277182755",
"name": "POMODORI SECCHI",
"price": "3.49",
"department": "ORTOFRUTTA",
"sku": "ART-9912"
}
],
"mode": "upsert"
}
Field Required Notes
ean yes Non-empty
name yes Non-empty after trim
price yes See price format
department no Department label
sku no Mapped to internal code

Recommended header: Idempotency-Key.

200 response: see Batch response. With a price diff on existing EANs: status: "pending_review" + reviewId.

Updates only prices of products already present (by EAN).

Request:

{
"items": [
{ "ean": "8051277182755", "price": "3.29" }
],
"queuePolicy": "snapshot_only",
"reviewPolicy": "require_review"
}
Field Default Notes
items[].ean Required
items[].price Required
queuePolicy snapshot_only Especially with apply_immediate
reviewPolicy require_review In-app staging

Unknown EAN → row in rejected (product_not_found); does not fail the entire batch.

Common schema for catalog and prices POSTs:

{
"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"
}
]
}
Field Meaning
accepted Accepted / merged rows
created New products (typically catalog)
updated Updated products
status pending_review or applied
reviewId Review UUID (if pending_review)
rejected[] Rows not applied, with machine code

PriceTag does not return the full snapshot in the response.

HTTP When
200 Batch processed (even with non-empty rejected)
400 Invalid body / schema
401 Missing or invalid token
403 Token for another store
404 Store or product (GET) not found
409 Idempotency-Key reused with a different body
422 Ops not applicable to current state
429 Rate limit exceeded

Typical codes in rejected[].code:

Code Context
invalid_ean Missing / empty EAN
invalid_name Missing name (catalog)
invalid_price Unparseable price
product_not_found EAN absent in the store (/prices)
unchanged_price Price already equal to shelf (/prices + review)
skipped Rows skipped by catalog merge
Limit Value
Items per POST (catalog / prices) max 1,000
Rate limit 60 requests / minute per token
Total products per store max 5,000
Idempotency-Key max 128 characters; response TTL 24 h

Beyond limits: 400 (batch too large) or 429 (rate).

Replace STORE_ID and TOKEN.

Finestra del terminale
curl -sS \
-H "Authorization: Bearer $TOKEN" \
"https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/health"
Finestra del terminale
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"
Finestra del terminale
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"

Typical response: "status": "pending_review", "reviewId": "...". In the app the Nuovi prezzi modal appears.

Finestra del terminale
curl -sS \
-H "Authorization: Bearer $TOKEN" \
"https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/products/8051277182755"
  1. Obtain storeId and create the token under Connessioni.
  2. Store the token in a vault; do not commit it.
  3. GET .../healthok: true.
  4. Push a few test EANs (catalog or prices) with Idempotency-Key.
  5. Check accepted / rejected and, if prices changed, pending_review.
  6. In the app: Nuovi prezzi modal → Conferma or Scarta.
  7. After Conferma: Invia alla coda (if ESLs are present) to publish to the shelf.
  8. In automation: batch ≤ 1000, max 60 req/min, retry only with the same Idempotency-Key when the body matches.
  • ESL label CRUD, store plan, departments as a dedicated resource
  • Base Station or BLE publish / ACK in the same request
  • Catalog replace_all
  • Outbound PriceTag → ERP webhooks
  • Writing prices back to the ERP
  • Billing API / multi-token with granular permissions (roadmap)
  • Per-vendor ERP contract variants
  • OpenAPI contract: /integration-api-v1.openapi.yaml
  • Onboarding / trial token: contact PriceTag (storeId + Connessioni guide)
  • Production incidents: provide storeId, UTC timestamp, Idempotency-Key, and body (without plaintext token)