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.
What it does (and does not)
Section titled “What it does (and does not)”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.
Prerequisites
Section titled “Prerequisites”- PriceTag account with access to the target store.
storeId— store UUID (in-app or provided during onboarding).- 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).
- Ability to make
HTTPSPOST/GETwith anAuthorizationheader and JSON body.
Base URL
Section titled “Base URL”| 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/healthOn POST requests use Content-Type: application/json.
Authentication
Section titled “Authentication”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.
Key concepts
Section titled “Key concepts”Product key = EAN
Section titled “Product key = EAN”All partner products are addressed by EAN (string, trimmed). Internal PriceTag IDs are not part of the contract.
JSON fields in English
Section titled “JSON fields in English”| 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.
Price format
Section titled “Price format”| 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.
Partial success
Section titled “Partial success”Batch POSTs usually respond with HTTP 200 even if some rows fail. Always check:
accepted/created/updatedrejected[]with{ ean, code, message }
Invalid body → 400. Operations not applicable to the snapshot → 422.
Idempotency
Section titled “Idempotency”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.
Price flow and review
Section titled “Price flow and review”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).
- The partner sends a catalog or prices
POST. - The API responds
200withstatus: "pending_review"andreviewId(if there is a price diff). - In the app: Nuovi prezzi modal → Conferma or Scarta.
- After Conferma: Invia alla coda to publish to ESLs (if assigned).
Review policy
Section titled “Review policy”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. |
Catalog and prices
Section titled “Catalog and prices”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 |
ESL queue
Section titled “ESL queue”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.
Endpoints
Section titled “Endpoints”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) |
Health
Section titled “Health”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.
Product read
Section titled “Product read”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.
Catalog (upsert)
Section titled “Catalog (upsert)”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.
Prices
Section titled “Prices”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.
Batch response
Section titled “Batch response”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 status codes
Section titled “HTTP status codes”| 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 |
Rejected codes
Section titled “Rejected codes”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 |
Limits
Section titled “Limits”| 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).
curl examples
Section titled “curl examples”Replace STORE_ID and TOKEN.
Health
Section titled “Health”curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/health"Catalog (upsert)
Section titled “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 (default review)
Section titled “Prices (default review)”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.
Product read
Section titled “Product read”curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/products/8051277182755"Integration checklist
Section titled “Integration checklist”- Obtain
storeIdand create the token under Connessioni. - Store the token in a vault; do not commit it.
GET .../health→ok: true.- Push a few test EANs (catalog or prices) with
Idempotency-Key. - Check
accepted/rejectedand, if prices changed,pending_review. - In the app: Nuovi prezzi modal → Conferma or Scarta.
- After Conferma: Invia alla coda (if ESLs are present) to publish to the shelf.
- In automation: batch ≤ 1000, max 60 req/min, retry only with the same
Idempotency-Keywhen the body matches.
Out of scope for v1
Section titled “Out of scope for v1”- 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 and support
Section titled “OpenAPI and support”- 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)