# Guide to project44 Webhooks: Features and Best Practices

project44 webhooks push shipment visibility data to your systems as it changes: a new tracking event, a revised ETA, a shipment created or deleted, an exception raised. If you run a transportation management system, a customer-facing tracking page or a control tower on project44 data, webhooks deliver those updates without polling the tracking APIs.

This guide covers how project44 push configurations work, the difference between standard and Granular webhooks, how to authenticate requests with an API key header, project44's ordered retry queue and what each response code does, and the best practices for a production receiver.

## What are project44 webhooks?

project44 calls a webhook "an endpoint hosted by you that you have registered to receive shipment updates on". You register it by sending a push configuration to `PUT /api/v4/webhooks`, then ask your project44 representative to complete the setup. From then on, changes to your shipments are pushed to the endpoint over HTTP.

There are two kinds of shipment webhook:

* **Standard webhooks** push the whole shipment each time something changes. The body is the shipment, with no event type field.
* **Granular Webhooks** (formerly Unified Webhooks) push small, typed deltas. You choose a type in `webhookConfiguration.webhookEventType`, and each message says exactly what changed. project44 recommends them for shipments, for their smaller payloads and filtering.

Orders and Loads have their own webhook setup guide and aren't covered here.

The HTTP method is part of the configuration: `GET`, `POST`, `PUT` or `PATCH`, defaulting to `POST`. project44 doesn't sign payloads. It offers five authentication types for the requests it sends: `API_KEY`, `BASIC`, `OAUTH2`, `MTLS` and `AD_OAUTH2_CERT`. This guide uses `API_KEY`, which adds a header you name with a value you choose, and is what Hookdeck's project44 source checks.

## project44 webhook features

| Feature | Details |
| --- | --- |
| Configuration | `PUT /api/v4/webhooks`, then setup is completed by your project44 representative |
| Webhook kinds | Standard (whole shipment) and Granular (typed deltas) |
| Granular types | Nine `shipments.v1.*` types |
| Method | `GET`, `POST`, `PUT` or `PATCH`, default `POST` |
| Authentication | `API_KEY` (up to two headers), `BASIC`, `OAUTH2`, `MTLS`, `AD_OAUTH2_CERT` |
| Signature | None |
| Filtering | Payload filtering with `fieldMasks`, and change filtering |
| Success | Any `2xx` |
| Retries | `408`, `429`, `502`, `503`, `504` and connection failures, every few minutes for up to 48 hours |
| Not retried | `3xx`, other `4xx`, and other `5xx` including `500` |
| Timeout | 20 seconds |
| Ordering | Messages are delivered in order. Unrelated messages may be sent concurrently, up to 12 at a time |
| Duplicates | Possible when a response is lost |

## Common events

Granular Webhooks carry the type in `eventType`:

| `eventType` | Fires when |
| --- | --- |
| `shipments.v1.shipment.created` | A new shipment is created (sent once) |
| `shipments.v1.shipment.deleted` | A shipment is deleted |
| `shipments.v1.event.added` | A new event is added to a shipment |
| `shipments.v1.event.updated` | An existing event is updated |
| `shipments.v1.event.removed` | An event is removed from a shipment |
| `shipments.v1.state.added` | A new 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 |

Each Granular message has the same envelope. Here is project44's example for an added event (the share link is elided in their docs):

```json
{
  "eventId": "333d2a1b-d332-4775-8f8e-960e5f538431",
  "eventType": "shipments.v1.event.added",
  "createdDateTime": "2025-03-31T17:01:42.92601441Z",
  "dataType": "EventChange",
  "context": {
    "shipment": {
      "shipmentId": "ac694c91-e308-4329-9584-ee10acb937b3",
      "identifiers": [{ "type": "BILL_OF_LADING", "value": "BOL123" }],
      "attributes": [],
      "createdDateTime": "2025-03-31T17:01:42.92601441Z",
      "lastModifiedDateTime": "2025-03-31T17:01:42.92601441Z",
      "shipmentShareLink": "..."
    }
  },
  "data": {}
}

```

`eventId` identifies the message and makes a good idempotency key. `context` carries a minimal shipment context by default, and you can ask for more with `includeContext` in the configuration. `data` holds the change itself, which varies by type.

Standard webhooks have no `eventType`. If one endpoint receives both kinds, check for `eventType` and treat bodies without it as standard shipment pushes.

> **Inspect project44 webhooks as they arrive.** Point a project44 push configuration at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to inspect and replay real deliveries, with no account or setup required.

## Setting up project44 webhooks

Generate a long random value for the API key header first (`openssl rand -hex 32` works), and store it in your app's environment.

Send a push configuration with the `API_KEY` authentication method. For a Granular Webhook, add `webhookConfiguration`:

```http
PUT /api/v4/webhooks
Content-Type: application/json

{
  "configName": "shipment-events",
  "url": "https://example.com/webhooks/project44",
  "method": "POST",
  "methodParams": {},
  "payloadFormat": "JSON",
  "webhookConfiguration": { "webhookEventType": "shipments.v1.event.added" },
  "authenticationMethods": [
    {
      "type": "API_KEY",
      "parameters": {
        "api_key_name": "X-Api-Key",
        "api_key": "your-long-random-secret"
      }
    }
  ]
}

```

project44 creates one header per API key, up to two, named by `api_key_name`. Its field table calls the value `api_key_value`, but its own example uses `api_key`. If the header doesn't arrive, check which field your account expects with project44. Use the base URL for your region (Americas or Europe), and authenticate the configuration request with your project44 API credentials.

After the configuration is accepted, contact your project44 representative to complete the setup. Use `GET /api/v4/webhooks` or `GET /api/v4/webhooks/name/{webhookName}` to check what's configured. Anything in `methodParams` is sent as query parameters on the URL.

For local development, use the [Hookdeck CLI](/docs/cli): `hookdeck listen 3000 project44 --path /webhooks/project44` gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required.

**With Hookdeck.** Create a project44 source in Hookdeck, enter your `api_key_name` as the **API Key Header** and the key as the **API Key**, and use the source URL as the configuration's `url`. The source pre-selects `POST` as the allowed method. If your configuration uses `GET`, `PUT` or `PATCH`, allow that method on the source too. The source checks the `API_KEY` method only, so don't rely on it with `BASIC`, `OAUTH2` or mTLS configurations.

## Securing project44 webhooks

Compare the API key header with your stored value. Look it up case-insensitively, compare the whole value in constant time, and check lengths first because `crypto.timingSafeEqual` throws on buffers of different lengths. Since the method is configurable, accept the one you chose, and handle an empty body if it's `GET`.

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

const app = express();

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyProject44Request(headers) {
  const secret = process.env.PROJECT44_WEBHOOK_SECRET;
  if (!secret) return false; // fail closed when unconfigured
  const name = (process.env.PROJECT44_WEBHOOK_HEADER || 'X-Api-Key').toLowerCase();
  const received = headers[name];
  return typeof received === 'string' && safeEqual(received, secret);
}

app.all('/webhooks/project44', express.raw({ type: '*/*' }), async (req, res) => {
  if (!['GET', 'POST', 'PUT', 'PATCH'].includes(req.method)) {
    return res.sendStatus(405);
  }
  if (!verifyProject44Request(req.headers)) {
    return res.sendStatus(401); // project44 won't retry a 401
  }

  let message = null;
  if (Buffer.isBuffer(req.body) && req.body.length > 0) {
    try {
      message = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
  }

  try {
    await enqueue(message); // persist, then process asynchronously
  } catch {
    return res.sendStatus(503); // retriable: project44 queues and retries
  }
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_project44_request(headers) -> bool:
    secret = os.environ.get("PROJECT44_WEBHOOK_SECRET")
    if not secret:
        return False
    name = os.environ.get("PROJECT44_WEBHOOK_HEADER", "X-Api-Key")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

```

An API key header authenticates the sender, not the body, and offers no replay protection, since nothing is signed. Serve the endpoint over HTTPS, keep the key out of logs, and rotate it with a new `PUT` if it leaks. project44 allows two API keys on one configuration, which lets you add a new key before removing the old one. If your security team needs stronger authentication on the connection itself, project44 also supports mutual TLS.

> **Make project44 webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your API key header on every request, acknowledges project44 straight away so its ordered queue never backs up behind your outage, and retries delivery to your service on your own schedule.

## project44 webhook limitations and pain points

### The wrong status code loses data

**The Problem:** A `500` from a bug in your handler, or a `401` from a misconfigured key, marks the message as failed for good. project44 moves on to the next message and never retries it.

**Why It Happens:** project44 retries only `408`, `429`, `502`, `503`, `504` and connection failures. Every `3xx`, every other `4xx`, and every other `5xx`, including `500`, is treated as a rejection that retrying can't fix.

**Workarounds:**

* Return `503` (or `429` when you're overloaded) for transient problems, never `500`.
* Catch unexpected errors in your handler and map them to a retriable status.
* Alert on `4xx` responses from your own endpoint, since those messages are gone.

**How Hookdeck Can Help:** Hookdeck accepts each message with a `2xx` as soon as it's stored, so a bug or misconfiguration on your side never turns into a message project44 discards. Hookdeck then delivers to your service with [automatic retries](/docs/retries), and you can replay any stored message once the fix is out.

### One slow message holds up the queue

**The Problem:** When a request times out or gets a retriable error, later messages wait behind it, so a single problem delays all your shipment updates.

**Why It Happens:** project44 delivers messages in order. When your endpoint looks down, it queues everything behind the failed message, retries every few minutes, and holds messages for up to 48 hours before expiring them.

**Workarounds:**

* Acknowledge within a few seconds, well inside the 20-second timeout, and process asynchronously.
* Support up to 12 concurrent requests, as project44 recommends, so unrelated messages aren't serialised.

**How Hookdeck Can Help:** Hookdeck answers project44 immediately, so its queue keeps moving. You control delivery to your own service, including [delivery rate](/docs/destinations#set-a-max-delivery-rate) and [timeouts](/docs/destinations#set-a-delivery-timeout).

### No signature on the payload

**The Problem:** A request with the right API key header looks the same as a real shipment update, and its body can't be integrity-checked.

**Why It Happens:** All five authentication types authenticate the connection or the request. None signs the payload.

**Workarounds:**

* Use a long random key and compare it in constant time.
* Use mutual TLS if your infrastructure supports it.

**How Hookdeck Can Help:** Hookdeck's project44 source compares the header you name against the exact value you enter. A request without a matching header still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. Every request is stored with its headers and body.

### Duplicates after network failures

**The Problem:** A message you've already processed can arrive again.

**Why It Happens:** project44 says it won't normally resend a message after a successful response, but a lost response, for example from a network failure, causes a resend.

**Workarounds:**

* Deduplicate Granular messages on `eventId`.
* For standard pushes, compare the shipment's last-modified time with what you've stored.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated Granular messages based on `eventId` before they reach your handler.

## Best practices

### Prefer Granular Webhooks for shipments

Typed deltas are smaller, easier to route and easier to deduplicate than whole-shipment pushes. Combine them with filters to receive only the event types and transport modes you need.

### Return codes deliberately

Use `2xx` once a message is safely stored, `503` or `429` when you want project44 to retry, and never `500`. Remember that `401` means project44 drops the message.

### Acknowledge fast and process asynchronously

Persist the message and answer within a few seconds, then process it in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent and order-aware

Use `eventId` as the idempotency key and compare `createdDateTime` or `lastModifiedDateTime` before overwriting newer data, since concurrent requests for unrelated shipments can interleave. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

project44 pushes shipment updates to a configured endpoint, either as whole shipments or as typed Granular deltas identified by `eventType`, with no payload signature. Authenticate with an `API_KEY` header compared in constant time over HTTPS, choose your response codes carefully because most errors are never retried, and keep acknowledgements fast so the ordered queue doesn't stall.

[Hookdeck Event Gateway](/event-gateway) checks the API key header, acknowledges every message as soon as it's stored, and retries delivery to your service on your own terms, which protects you from project44's no-retry rules for `4xx` and `500` responses.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle project44 webhooks reliably in minutes.