Gareth Wilson Gareth Wilson

Guide to project44 Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationPUT /api/v4/webhooks, then setup is completed by your project44 representative
Webhook kindsStandard (whole shipment) and Granular (typed deltas)
Granular typesNine shipments.v1.* types
MethodGET, POST, PUT or PATCH, default POST
AuthenticationAPI_KEY (up to two headers), BASIC, OAUTH2, MTLS, AD_OAUTH2_CERT
SignatureNone
FilteringPayload filtering with fieldMasks, and change filtering
SuccessAny 2xx
Retries408, 429, 502, 503, 504 and connection failures, every few minutes for up to 48 hours
Not retried3xx, other 4xx, and other 5xx including 500
Timeout20 seconds
OrderingMessages are delivered in order. Unrelated messages may be sent concurrently, up to 12 at a time
DuplicatesPossible when a response is lost

Common events

Granular Webhooks carry the type in eventType:

eventTypeFires when
shipments.v1.shipment.createdA new shipment is created (sent once)
shipments.v1.shipment.deletedA shipment is deleted
shipments.v1.event.addedA new event is added to a shipment
shipments.v1.event.updatedAn existing event is updated
shipments.v1.event.removedAn event is removed from a shipment
shipments.v1.state.addedA new state is added to a shipment
shipments.v1.state.updatedAn existing state is updated
shipments.v1.state.removedA state is removed from a shipment
shipments.v1.exception.addedOrUpdatedAn 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 (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, 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.


Gareth Wilson

Gareth Wilson

Product Marketing

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