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):
{
"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 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:
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: 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.
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:
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 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(or429when you're overloaded) for transient problems, never500. - Catch unexpected errors in your handler and map them to a retriable status.
- Alert on
4xxresponses 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, 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 and timeouts.
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 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.
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.
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 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 for free and handle project44 webhooks reliably in minutes.