# parcelLab Webhooks

parcelLab (post-purchase tracking and delivery notifications) sends a webhook to your HTTPS
endpoint each time a consignment changes status. Payloads are **not signed**. parcelLab sets
webhooks up for you from the URL and credentials you give it, and supports three
authentication mechanisms: HTTP Basic, access token headers and OAuth 2.0. This skill covers
**HTTP Basic**.

## When to Use This Skill

* How do I receive parcelLab webhooks?
* How do I verify the parcelLab `Authorization: Basic` header?
* Does parcelLab sign webhook payloads? (No: shared credentials only)
* Why does my parcelLab webhook endpoint return 401?
* How do I handle `onDispatch`, `onOutForDelivery` or `onDelivered` events?

## Verification (core)

Give parcelLab a username and password along with your HTTPS URL. It sends them as
`Authorization: Basic base64(username:password)`. Split the decoded value on the **first**
colon, then compare username and password each in constant time, evaluating both before
deciding:

```javascript
const crypto = require('crypto');

const sha = (buf) => crypto.createHash('sha256').update(buf).digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(sha(a), sha(b)); // equal-length digests

function verifyParcellabRequest(headers, config = process.env) {
  const { PARCELLAB_WEBHOOK_USERNAME: user, PARCELLAB_WEBHOOK_PASSWORD: pass } = config;
  if (!user || !pass) return false;                      // never accept when unconfigured
  const m = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(headers['authorization'] || '');
  if (!m) return false;
  const decoded = Buffer.from(m[1], 'base64');
  const colon = decoded.indexOf(0x3a);                   // first colon only
  if (colon < 0) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(user));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(pass));
  return userOk && passOk;                               // no short-circuit
}

```

Return `401` for a missing or malformed header or any mismatch, `400` for a body that is not a
JSON object, and `200` once handled. Read the body raw (`express.raw({ type: '*/*' })`) so any
content type works, and parse JSON only after the credential check. Python and Next.js
equivalents are in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/parcellab-webhooks/references/verification.md).

> **Static credentials authenticate the sender, not the body.** There is no signature and no
> timestamp, so a captured request can be replayed and the body can't be proven untampered.
> Use HTTPS only (parcelLab doesn't support plain HTTP), keep the credential secret, and
> rotate it if it leaks. Deduplicate on `eventid`.

> **Credentials must arrive on the first request.** The docs don't say whether parcelLab sends
> Basic credentials preemptively, so assume it does. A `401` + `WWW-Authenticate` challenge
> flow won't work, and it won't work through Hookdeck either, which verifies the request as
> received.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/parcellab-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/parcellab-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/parcellab-webhooks/examples/fastapi/).

## Event Types

The event name is in the `event` field of the JSON body:

| Event | Fires when |
| --- | --- |
| `onCreated` / `onUpdated` | A record is created or updated from retailer data |
| `onDispatch` | First carrier scan (once per consignment) |
| `onScheduled` | Carrier announces or changes the delivery day or window |
| `onOutForDelivery` | Consignment is out for delivery |
| `onDelay` / `onInternalDelay` | Carrier delay, or the consignment wasn't shipped in time |
| `onFailedAttempt` | Delivery attempted but recipient not met or location not accessible |
| `onPickupReady` | Ready for collection at an access point or locker |
| `onDelivered` | Final: delivered |
| `onReturn` | Final: being returned to the sender |

parcelLab can also send **CloudEvents** format, where `type` is
`com.parcellab.tracking.event.<lowercased event>` and the data sits under `data`. The examples
handle both shapes. There is no event deduplication on parcelLab's side.

## Environment Variables

```bash
PARCELLAB_WEBHOOK_USERNAME=parcellab
PARCELLAB_WEBHOOK_PASSWORD=replace-with-a-long-random-password

```

## Local Development

```bash
npx hookdeck-cli listen 3000 parcellab --path /webhooks/parcellab

```

No account required. Give the printed URL, plus your Basic credentials, to parcelLab as the
webhook target.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/parcellab-webhooks/references/overview.md) - Events, payload structure, retries, CloudEvents format
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/parcellab-webhooks/references/setup.md) - Giving parcelLab your URL and Basic credentials
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/parcellab-webhooks/references/verification.md) - Basic auth check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/parcellab-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests