# project44 Webhooks

project44 (supply chain visibility) pushes shipment updates to an HTTPS endpoint you host. The
webhook is configured through the API with `PUT /api/v4/webhooks` and an authentication method of
`API_KEY`, `BASIC`, `OAUTH2`, `MTLS` or `AD_OAUTH2_CERT`. This skill covers **`API_KEY`**: project44 adds a header
you name, with a value you choose, to every request.

project44 webhooks are **not signed**. There is no HMAC and no timestamp. Your endpoint
authenticates a request by comparing that header with the secret you stored.

## When to Use This Skill

* How do I receive project44 webhooks?
* How do I authenticate project44 webhook requests (`API_KEY` header)?
* Does project44 sign webhook payloads? (No: use the shared header value)
* How do I handle `shipments.v1.event.added` or `shipments.v1.state.updated` events?
* Why does my project44 webhook endpoint return 401, and why did project44 stop retrying?

## Verification (core)

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

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyProject44Request(headers, env = process.env) {
  const secret = env.PROJECT44_WEBHOOK_SECRET;
  if (!secret) return false;                                   // never accept when unconfigured
  const name = (env.PROJECT44_WEBHOOK_HEADER || 'X-Api-Key').toLowerCase();
  const received = headers[name];                              // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);     // WHOLE value, no Bearer parsing
}

```

* The header name is whatever you set as `api_key_name` (the examples default to `X-Api-Key` and
  read `PROJECT44_WEBHOOK_HEADER`). Look it up case-insensitively.
* `PROJECT44_WEBHOOK_SECRET` is the **exact, whole header value**, including any `Bearer ` prefix you
  typed. Compare the whole value; don't strip or parse a prefix.
* Missing header or mismatch: `401`. Authenticate before parsing the body.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS only, make the handler idempotent (use
  `eventId`), and rotate the credential if it leaks.
* The webhook method is configurable (`GET`, `POST`, `PUT`, `PATCH`; `POST` is the default), so the
  examples accept all four and tolerate an empty body. Hookdeck's project44 source pre-selects
  only `POST`: allow any other method you use on the source.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse JSON only
  after the credential check.

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

## Event Types

Granular webhooks carry an `eventType` field (the same value you choose as
`webhookConfiguration.webhookEventType` when configuring the webhook). Other fields:
`eventId`, `createdDateTime`, `dataType`, `context` and `data`.

| `eventType` | Sent When |
| --- | --- |
| `shipments.v1.shipment.created` | A new shipment is created (once) |
| `shipments.v1.shipment.deleted` | A shipment is deleted |
| `shipments.v1.event.added` | An event is added to a shipment |
| `shipments.v1.event.updated` | An existing shipment event is updated |
| `shipments.v1.event.removed` | A shipment event is removed |
| `shipments.v1.state.added` | A state is added to a shipment |
| `shipments.v1.state.updated` | An existing state is updated |
| `shipments.v1.state.removed` | A state is removed from a shipment |
| `shipments.v1.exception.addedOrUpdated` | An exception is added or updated |

Non-granular (standard) shipment webhooks use a different payload with no `eventType`; the
examples acknowledge them as unhandled.

## Response Codes Matter

project44 treats `2xx` as delivered. It retries only `408`, `429`, `502`, `503`, `504` and
connection failures (queued up to 48 hours, in order, so a retriable failure blocks later
messages). **`401`, other `4xx`, `3xx` and `500` are not retried: the message is dropped.** So the
examples return `503` (not `500`) when your own handler fails, and a wrong secret loses messages.
Respond within a few seconds; the timeout is 20 seconds.

## Environment Variables

```bash
PROJECT44_WEBHOOK_HEADER=X-Api-Key        # the api_key_name you configured (default X-Api-Key)
PROJECT44_WEBHOOK_SECRET=your-secret      # the exact api key value, including any "Bearer " prefix

```

## Local Development

project44 can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

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

```

Use the printed URL as the webhook `url` in the project44 configuration.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/project44-webhooks/references/overview.md) - What project44 webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/project44-webhooks/references/setup.md) - Configure the webhook with an `API_KEY` header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/project44-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/project44-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests