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.
1. Provisioning
- In the PassportLab dashboard, go to Integrations.
- Click Configure Webhook on the Generic Webhook tile.
- Give the integration a label (e.g. SAP S/4HANA Production).
- On creation, a one-time dialog reveals the Webhook URL and Signing secret. Copy both immediately — the secret is shown only once.
- (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 limit | 60 requests / minute / integration |
|---|---|
| Max records per call | 10 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 (pending → processing → complete / 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:
| Field | Example | Meaning |
|---|---|---|
source_path | $.MaterialDescription | JSONPath into your ERP payload. Bracket / dot notation both supported. |
target_field | productName | Dotted path into the DPP <code>passportData</code> object. |
transform | trim, upper, number, iso_date, regex:<p>:<r> | Optional value transformation. |
default | "DE" | Used when the source path is missing. |
6. Error responses
| Status | Meaning | What to do |
|---|---|---|
| 401 — Missing X-PassportLab-Timestamp | Signature header sent, no timestamp | Add the timestamp header. |
| 401 — Outside the 5-minute tolerance | Clock drift > 5 min | Sync your ERP server's clock via NTP. |
| 401 — Signature mismatch | HMAC digest doesn't match | Verify secret + that you signed timestamp + "." + raw body. |
| 400 — Body must be {"records": [...]} | Wrong-shape envelope | Ensure the JSON body uses the records array. |
| 403 — Not authorized for this integration | Bearer cookie from a different org | Use the HMAC signature instead. |
| 404 — Integration not found | Wrong integration_id in URL | Check the integrations dashboard. |
| 413 — Max 10 000 records / call | Too many records | Batch into multiple calls. |
| 429 — Rate limited | > 60 req / minute / integration | Back 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
| Symptom | Likely cause |
|---|---|
401 Signature mismatch every time | Your code's signing base probably doesn't match f"{ts}." + raw_body. Print both and compare bytes. |
| Works in curl, fails in code | Some 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 fields | Your 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 minute | You'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.