Gareth Wilson Gareth Wilson

Guide to parcelLab Webhooks: Features and Best Practices

Published


parcelLab is a post-purchase platform for retailers. It tracks shipments across carriers, turns raw carrier scans into a common status model, and powers the order status pages and delivery notifications shoppers see. Its webhooks feed the same tracking events back into your shop systems, so your order management, customer service and analytics tools know when a parcel ships, is delayed, goes out for delivery or arrives.

This guide covers the events parcelLab sends and their payload, how to authenticate requests with HTTP Basic credentials, parcelLab's retry and deduplication behaviour, and the practices that keep a receiver reliable in production.

What are parcelLab webhooks?

parcelLab sends one webhook per event for each consignment, meaning each tracking record in parcelLab. A split order with several trackings produces several webhooks: each tracking gets its own onDispatch, even when they ship together.

You don't register webhooks yourself. You give parcelLab an HTTPS URL and credentials, and parcelLab sets the webhook up. Plain HTTP is not supported.

parcelLab does not sign requests. It supports three ways to authenticate them: HTTP Basic authentication, access token headers and an OAuth 2.0 access token. This guide uses HTTP Basic, which is what Hookdeck's parcelLab source checks.

parcelLab webhook features

FeatureDetails
ConfigurationProvide parcelLab with an HTTPS URL and credentials. parcelLab configures the webhook
ScopeOne webhook per event per consignment
Method and formatPOST, Content-Type: application/json
DiscriminatorThe body's event field, such as onDelivered
FormatsparcelLab JSON (default) or CloudEvent
AuthenticationHTTP Basic, access token headers or OAuth 2.0 access token. No signature
DeduplicationNone on parcelLab's side. Use eventid
Retries15 retries at 30 minute intervals after an error, then the event is logged and dropped
Source IPs35.156.240.70, 35.157.215.87, 35.157.195.174

Events

EventFires when
onInternalDelayThe consignment was not shipped in time, against the send_date set on record creation
onCreatedA record is created through data transfer from the retailer
onUpdatedA record is updated through data transfer from the retailer. The updated fields are not included
onDispatchThe first carrier scan. Sent only once
onScheduledThe carrier announces a delivery day, and again for every change to it
onDelayA delay or delivery issue, with an exception_code
onOutForDeliveryThe consignment is out for delivery
onFailedAttemptA delivery attempt failed, with next_action (NewAttemptToday, NewAttemptNextDay or PickUpReady)
onPickupReadyThe parcel is ready for collection at a courier location, with pickup location fields
onDeliveredFinal event: the consignment was delivered
onReturnFinal event: delivery failed and the parcel is going back to the sender, with a return_reason

parcelLab does not deduplicate events, so several carrier scans of the same type send several webhooks. Expect more than one onOutForDelivery for one parcel.

Every event has the same top-level structure. Here is parcelLab's onDelivered example, with the tracking link shortened as in the docs:

{
  "event": "onDelivered",
  "eventid": "abc123-def456",
  "timestamp": "2000-12-16T16:12:00.000Z",
  "references": {
    "courier": "dhl-germany",
    "tracking_number": "00340000000001",
    "orderNo": "ORD0012345",
    "client": "parcelFashion",
    "warehouse": "EU",
    "destination_country_iso3": "DEU",
    "language": "de"
  },
  "data": {
    "tracking_link": "https://www.dhl.de/...?piececode=00340000000001",
    "reroute_link": "",
    "raw_scan": "DLVRD-ACCPT-ZU | parcelLab",
    "raw_scan_location": "Munich",
    "delivery_status": "Delivered",
    "delivery_status_locale_short": "Zugestellt",
    "delivery_status_locale_long": "Die Lieferung wurde der Empfängerin oder dem Empfänger (parcelLab) zugestellt.",
    "expected_delivery_date_raw": "",
    "expected_delivery_date_formatted": "",
    "delivery_location": "Doorstep",
    "delivery_information": "parcelLab"
  },
  "reporting": {
    "lead_time_door_to_door": 2,
    "lead_time_qualified_attempt": 2,
    "lead_time_end_to_end": 3
  }
}

eventid is a unique ID for the event, made of a-f, 0-9 and dashes, and parcelLab says to use it for deduplication. timestamp is when the event happened, not when the webhook was sent. references identifies the consignment and your order, and the same values can optionally be appended to the URL as query parameters. reporting holds lead times, which are -1 until the scan that ends them has happened. raw_scan is whatever the carrier sent, with no common pattern.

In the CloudEvent format, the event becomes type, such as com.parcellab.tracking.event.ondelivered, the eventid becomes id, and references, data and reporting move inside the CloudEvent's data object. Which format you receive is part of the setup parcelLab does for you.

Inspect parcelLab webhooks as they arrive. Give parcelLab a Hookdeck Inspect URL during setup to see the exact headers and body it sends, with no account or setup required.

Setting up parcelLab webhooks

Pick a username and generate a long random password, for example with openssl rand -hex 32, and store both on your receiver.

  1. Prepare an HTTPS endpoint with a valid certificate.
  2. Give parcelLab the URL and the Basic credentials, and say which format you want (parcelLab JSON or CloudEvent) and whether you want the references values as query parameters.
  3. If you allowlist at the firewall, allow 35.156.240.70, 35.157.215.87 and 35.157.195.174.
  4. Once parcelLab has set the webhook up, create a test consignment and watch for onCreated.

The credentials must arrive on the first request. parcelLab's documentation doesn't say whether it sends Basic credentials straight away or only after a 401 challenge with a WWW-Authenticate header. A sender that waits for a challenge will never authenticate against a receiver that doesn't issue one, so confirm with parcelLab that the Authorization header is sent on every request.

To put Hookdeck in front of your service, create a Hookdeck source with the parcelLab type and give parcelLab its URL. Enter the same Username and Password on the source. Hookdeck does not send a 401 challenge, so it needs the credentials on the first request. The source accepts POST, which is the method parcelLab uses. If parcelLab sends access token headers or an OAuth token instead of Basic credentials, this source won't verify them.

Securing parcelLab webhooks

With Basic authentication, each request carries Authorization: Basic base64(username:password). Decode it, split on the first colon, and compare the username and password with your stored values in constant time.

const crypto = require('crypto');
const express = require('express');

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header, username, password) {
  if (!username || !password) return false; // never accept when unconfigured
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon: passwords may contain ':'
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(username, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(password, 'utf8'));
  return userOk && passOk; // evaluate both before deciding
}

const app = express();

app.post('/webhooks/parcellab', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBasicAuth(
    req.get('authorization'),
    process.env.PARCELLAB_WEBHOOK_USERNAME,
    process.env.PARCELLAB_WEBHOOK_PASSWORD
  );
  if (!ok) return res.sendStatus(401);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Skip event.eventid if you have seen it, then route on event.event.
  switch (event.event) {
    case 'onDispatch':
    case 'onOutForDelivery':
    case 'onDelivered':
      break;
    default:
      // Log and acknowledge anything else.
  }

  res.sendStatus(200);
});

The same check in Python:

import base64
import binascii
import hmac
import os
import re

BASIC_PATTERN = re.compile(r"^basic\s+([A-Za-z0-9+/]+={0,2})\s*$", re.IGNORECASE)


def verify_basic_auth(header, username, password) -> bool:
    if not username or not password:
        return False  # never accept when unconfigured
    match = BASIC_PATTERN.match(header or "")
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True)
    except (binascii.Error, ValueError):
        return False
    received_user, sep, received_password = decoded.partition(b":")  # first colon only
    if not sep:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    user_ok = hmac.compare_digest(received_user, username.encode("utf-8"))
    password_ok = hmac.compare_digest(received_password, password.encode("utf-8"))
    return user_ok and password_ok


# verify_basic_auth(request.headers.get("authorization"),
#                   os.environ.get("PARCELLAB_WEBHOOK_USERNAME"),
#                   os.environ.get("PARCELLAB_WEBHOOK_PASSWORD"))

Basic credentials prove the sender knows the password. They do not cover the body and carry no timestamp of the request, so a modified or replayed request still passes. parcelLab already requires HTTPS. Use a long random password, rotate it with parcelLab if it might have leaked, and add the three source IPs at your firewall as a second layer.

Make parcelLab webhooks production-ready. Hookdeck Event Gateway checks the Basic credentials, drops repeat scans with deduplication, and stores every tracking event until your service has processed it.

parcelLab webhook limitations and pain points

Credentials have to arrive without a challenge

The Problem: Every request fails authentication even though the credentials are right.

Why It Happens: Some HTTP clients only send Basic credentials after a server answers 401 with a WWW-Authenticate challenge. parcelLab's docs don't say which way its sender works, and a receiver that verifies the request as it arrives, as Hookdeck does, never issues that challenge.

Workarounds:

  • Ask parcelLab to confirm the Authorization header is sent on the first request.
  • Point the webhook at a Hookdeck Inspect URL first and check the header is there.

How Hookdeck Can Help: Hookdeck keeps every request, including those that fail verification, with full headers, so you can see straight away whether Authorization arrived.

Repeated scans, repeated webhooks

The Problem: One parcel produces several onOutForDelivery or onScheduled webhooks, and a handler that sends a customer message on each one sends it several times.

Why It Happens: parcelLab doesn't deduplicate events. Every carrier scan of a type sends a webhook, and onScheduled fires on every change to the delivery date.

Workarounds:

  • Deduplicate on eventid for retries of the same event.
  • Decide per event type whether a repeat is news. A second onScheduled with a new date is, a second onOutForDelivery usually isn't.

How Hookdeck Can Help: Hookdeck deduplication can drop repeated deliveries based on fields you choose, such as eventid, before they reach your service.

Retries stop after about 7.5 hours

The Problem: An outage longer than the retry window loses tracking events, including final ones like onDelivered.

Why It Happens: After an error, parcelLab retries 15 times at 30 minute intervals, then logs an error and deletes the event from its queue.

Workarounds:

  • Alert on errors from your endpoint well within that window.
  • Reconcile open orders against parcelLab's API after an outage.

How Hookdeck Can Help: Hookdeck accepts and stores each event as it arrives, then delivers to your service with retries you configure, and you can replay any event once your service is back.

Events out of order

The Problem: A late retry can deliver an older status after a newer one, so an order flips back from delivered to out for delivery.

Why It Happens: Retries run 30 minutes apart, and each consignment's events are independent requests.

Workarounds:

  • Compare timestamp, which is when the event happened, with the latest you have stored before updating.
  • Treat onDelivered and onReturn as final and ignore earlier statuses after them.

Best practices

Key everything on eventid and the tracking

Use eventid for idempotency and references.courier plus references.tracking_number for the consignment. Map to your order through references.orderNo. See our guide to webhook idempotency.

Treat optional fields as optional

exception_code, next_action, the pickup location fields, delivery_information and return_reason only appear on their events, and some pickup details aren't available for every location. Read them with optional access.

Use the localised strings for customers

delivery_status_locale_short and delivery_status_locale_long are already in the recipient's language from references.language. They're safer to show customers than raw_scan, which is whatever the carrier sent.

Acknowledge fast

Return 200 once the request is authenticated and process it from a queue. See why to process webhooks asynchronously.

Conclusion

parcelLab sends a JSON webhook for each tracking event of each consignment, with no deduplication of carrier scans and a retry window of about 7.5 hours. Authenticate with Basic credentials that arrive on the first request, compare them in constant time, deduplicate on eventid, order by timestamp, and treat onDelivered and onReturn as final.

Hookdeck Event Gateway checks the Basic credentials, drops repeats, and holds each tracking event until your service has processed it.

Get started with Hookdeck for free and handle parcelLab webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.