PassportLabDevelopersWebhook integration

Push product data into PassportLab via webhook

A signed HTTP endpoint for streaming products from any ERP, PIM, or PLM. No PassportLab user login required — your ERP authenticates each request with an HMAC-SHA256 signature and a fresh timestamp.

HMAC-SHA256 signedReplay protected (±5 min)Async ARQ queueField mapping engine

1. Provisioning

  1. In the PassportLab dashboard, go to Integrations.
  2. Click Configure Webhook on the Generic Webhook tile.
  3. Give the integration a label (e.g. SAP S/4HANA Production).
  4. On creation, a one-time dialog reveals the Webhook URL and Signing secret. Copy both immediately — the secret is shown only once.
  5. (Optional but recommended.) Open Mapping rules on the integration card and map your ERP's field names to DPP passport fields. Built-in Test button shows the normalised output against a sample payload.

2. Request format

Method & headers

POST {webhookUrl}
Content-Type: application/json
X-PassportLab-Timestamp: <unix epoch seconds>
X-PassportLab-Signature: sha256=<hex digest>

Body

{
  "records": [
    { "name": "Widget Pro", "barcode": "01234567890123", ... },
    { "name": "Widget Lite", "barcode": "01234567890116", ... }
  ]
}

Each item in records is a free-form object — your mapping rules transform it into a DPP passport. If no rules are configured, records must already match the DPP passportData shape.

Limits

Rate limit60 requests / minute / integration
Max records per call10 000
Timestamp tolerance±5 minutes from server time

3. Signing

We use the Stripe-style signature scheme: timestamp + dot + body, HMAC-SHA256, hex digest, prefixed with sha256=.

signing_base = <timestamp> + "." + <raw request body bytes>
signature    = "sha256=" + HMAC-SHA256(secret, signing_base).hexdigest()

Including the timestamp in the signed payload is replay protection: even if a valid request leaks, it cannot be re-submitted outside the 5-minute window.

import hashlib, hmac, json, time, requests

INTEGRATION_ID = "REPLACE_ME"
SECRET         = "REPLACE_ME"  # from your credential vault
BASE_URL       = "https://api.passportlab.io"

records = [{"name": "Widget Pro", "barcode": "01234567890123"}]
body    = json.dumps({"records": records}).encode("utf-8")
ts      = str(int(time.time()))
sig     = "sha256=" + hmac.new(
    SECRET.encode(), f"{ts}.".encode() + body, hashlib.sha256
).hexdigest()

res = requests.post(
    f"{BASE_URL}/api/v1/integrations/{INTEGRATION_ID}/webhook-ingest",
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-PassportLab-Timestamp": ts,
        "X-PassportLab-Signature": sig,
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())  # → {"jobId": "...", "totalRecords": 1, "enqueued": true}

4. Response & polling

A successful request returns 202 Accepted:

{
  "jobId": "f4c1f2c8-7a51-4e3b-8c12-9bb9d8f00111",
  "totalRecords": 1,
  "enqueued": true
}
  • enqueued: true — the job was handed off to the ARQ worker.
  • enqueued: false — Redis is unreachable; the job is running inline as a fallback. Functionally identical, slower for large batches.

Poll for results

GET {baseUrl}/api/v1/jobs/{jobId}

Returns status (pendingprocessingcomplete / partial / failed), per-record counters, and a results array on completion.

5. Field mapping

If your ERP's payload shape differs from the DPP schema (it almost always will), define mapping rules in the dashboard. Each rule has four fields:

FieldExampleMeaning
source_path$.MaterialDescriptionJSONPath into your ERP payload. Bracket / dot notation both supported.
target_fieldproductNameDotted path into the DPP <code>passportData</code> object.
transformtrim, upper, number, iso_date, regex:<p>:<r>Optional value transformation.
default"DE"Used when the source path is missing.

6. Error responses

StatusMeaningWhat to do
401 — Missing X-PassportLab-TimestampSignature header sent, no timestampAdd the timestamp header.
401 — Outside the 5-minute toleranceClock drift > 5 minSync your ERP server's clock via NTP.
401 — Signature mismatchHMAC digest doesn't matchVerify secret + that you signed timestamp + "." + raw body.
400 — Body must be {"records": [...]}Wrong-shape envelopeEnsure the JSON body uses the records array.
403 — Not authorized for this integrationBearer cookie from a different orgUse the HMAC signature instead.
404 — Integration not foundWrong integration_id in URLCheck the integrations dashboard.
413 — Max 10 000 records / callToo many recordsBatch into multiple calls.
429 — Rate limited> 60 req / minute / integrationBack off or call less frequently.

7. Operational guidance

Idempotency

PassportLab uses the DPP code (derived from GTIN + serial number) as the unique identifier. Re-posting a record with the same GTIN+serial is skipped, not duplicated — the response marks it skipped with reason "DPP code 'DPP-…' already exists". Safe to retry on transient errors.

Retries

On 502 / 503 / 504 or network timeouts, retry with exponential backoff (1s, 2s, 4s, 8s, capped at 30s). Up to 3 retries is typically enough. Do not retry on 4xx responses — the request will fail again.

Batching

  • Small ERPs (< 1 000 SKUs): send the full catalogue nightly in one call.
  • Larger ERPs: send only the delta (rows changed since last sync), batched in groups of up to 1 000 records per request.

8. Troubleshooting

SymptomLikely cause
401 Signature mismatch every timeYour code's signing base probably doesn't match f"{ts}." + raw_body. Print both and compare bytes.
Works in curl, fails in codeSome HTTP clients re-serialise JSON before sending, changing whitespace. Always sign the exact bytes you put on the wire — compute the digest after JSON.stringify.
Records arrive but resulting DPPs have empty fieldsYour mapping rules don't cover those fields. Open the field-mapping editor, paste a sample, click Test, and check the normalised output.
429 once per minuteYou're sending one record per HTTP call. Batch into a single POST with records: [...].

Ready to push your first ERP record?

Start a free PassportLab account, configure a generic webhook, and have data flowing in under 10 minutes.