Developers

REST API & webhooks

Everything the dashboard does, from your ERP, PIM or label software. JSON in, JSON out, the same GS1 validation as the UI.

Authentication

Create a key under Integrations in your workspace (owner only). Send it as a bearer token. Keys are hashed at rest and shown once.

curl -H "Authorization: Bearer pql_live_…" https://paqlink.com/api/v1/me

Scopes: every key can read; tick write to create, update and delete products. Rate limit: 600 requests per minute per key (HTTP 429 beyond that).

Errors

{"error": {"code": "validation", "message": "GTIN check digit is invalid"}}

401 unauthorized · 403 forbidden (scope) / plan_limit · 404 not_found · 422 validation · 429 rate_limited.

Products

GET /api/v1/products?page=1&per_page=25 · GET /api/v1/products/{id} · POST /api/v1/products · PATCH /api/v1/products/{id} · DELETE /api/v1/products/{id}

curl -X POST https://paqlink.com/api/v1/products \
  -H "Authorization: Bearer pql_live_…" -H "Content-Type: application/json" \
  -d '{
    "gtin": "6001234567899",
    "name": "Chenin Blanc 2025",
    "brand": "Example Estate",
    "lot": "L2025A",
    "expiry_yymmdd": "271231",
    "default_target": "elabel",
    "elabel": {"langs": {"en": {"ingredients": "Grapes, sulphites", "alcohol": "12.5% vol",
                                 "nutrition": {"Energy": "289 kJ / 70 kcal"}}}},
    "extra_links": [{"type": "gs1:sustainabilityInfo", "url": "https://example.com/green", "title": "Recycling"}]
  }'

Response 201:

{"data": {
  "id": 42, "gtin14": "06001234567899", "name": "Chenin Blanc 2025",
  "digital_link_uri": "https://paqlink.com/01/06001234567899/10/L2025A?17=271231",
  "hosted_product_url": "https://paqlink.com/p/BEZAbUX6",
  "hosted_elabel_url": "https://paqlink.com/e/BEZAbUX6",
  "default_target": "elabel", "synced_at": "2026-08-28T17:05:12+00:00", …
}}

Fields: gtin (8–14 digits, check digit verified, stored 14-digit; immutable), name, brand, description, image_url, lot (AI 10), serial (AI 21), expiry_yymmdd (AI 17), default_target (product | elabel | custom), custom_url, extra_links[] (curated GS1 link types), elabel.langs.{code} (ingredients, allergens, alcohol, nutrition{}). Plan product limits apply exactly as in the dashboard.

QR codes & scans

GET /api/v1/products/{id}/qr.svg?x_dim=0.495 · GET /api/v1/products/{id}/qr.png (600 dpi) · GET /api/v1/products/{id}/scans?days=30{"data": {"total": 12, "daily": {"2026-08-27": 5, …}}}. Scan counters are daily aggregates only — no per-scan data exists to return.

Webhooks

Add an https endpoint under Integrations and pick events: product.created, product.updated, product.deleted, scan.daily_digest. Use “Send test event” to receive a ping.

POST https://example.com/paqlink
X-PaqLink-Event: product.created
X-PaqLink-Delivery: evt_1f3a…
X-PaqLink-Signature: sha256=…

{"id": "evt_1f3a…", "event": "product.created", "created_at": "…",
 "data": {"id": 42, "gtin14": "06001234567899", "name": "…", "default_target": "elabel", "slug": "BEZAbUX6"}}

Verify the signature — HMAC-SHA256 of the raw body with your webhook secret, constant-time compare:

# Python
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-PaqLink-Signature"])
// Node
const crypto = require("crypto");
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-paqlink-signature"]));

Deliveries are attempted once, synchronously, with a 5-second timeout; respond 2xx quickly and process asynchronously. Twenty consecutive failures disable the webhook (re-enable it from the dashboard). Use X-PaqLink-Delivery for idempotency on your side.

Resolver endpoints (no key needed)

The GS1-conformant resolver is public by design: GET /01/{gtin14} (307 to the default link), ?linkType=linkset or Accept: application/linkset+json for the RFC 9264 linkset, /.well-known/gs1resolver for the description file. See Standards.