# Guide to FourKites Webhooks: Features and Best Practices

FourKites webhooks, which FourKites calls callbacks, push shipment visibility data to your systems as it happens: a load is created, a truck's location updates, an ETA changes, the load arrives at or leaves a stop, a stop is completed, or proof of delivery is uploaded. If you track freight with FourKites, the supply-chain visibility platform, callbacks are how that data reaches your TMS, ERP or customer portal without polling.

This guide covers how FourKites callback subscriptions work, the main callback messages and their payloads, how to protect your endpoint with Basic credentials, FourKites's retry rules, and the best practices for production.

## What are FourKites webhooks?

FourKites sells its callbacks as a licensed product called **Standard Webhooks**. The name is FourKites's own, and the product has nothing to do with the open Standard Webhooks specification.

Each callback is an application event. When a subscribed event occurs, FourKites sends a real-time JSON payload in an HTTP `POST` to your endpoint. Subscriptions aren't self-serve: you supply FourKites with your URL, the callback messages you want, an authentication method, and any additional headers, and FourKites stores the configuration.

FourKites doesn't sign callbacks. It supports three authentication methods:

* **Basic**: HTTP Basic credentials.
* **API Key / Auth Header**: keys or secrets inside HTTP headers.
* **OAuth2** with the client credentials flow: FourKites fetches a token from your token URL using a `client_id` and `client_secret`, and expects the token in an `access_token` JSON key.

## FourKites webhook features

| Feature | Details |
| --- | --- |
| Product | FourKites Standard Webhooks, a licensed solution |
| Subscription | Arranged with FourKites: URL, callback messages, authentication method and additional headers |
| Method and format | `POST` with a JSON body |
| Event identifier | `MessageType` in the body |
| Authentication | Basic, API Key / Auth Header, or OAuth2 client credentials. No signature |
| Retries | `5xx` only, after 1 minute, 15 minutes, 1 hour and 24 hours |
| `4xx` responses | Never retried |
| Monitoring | FourKites doesn't monitor whether your endpoint succeeds |
| Source IPs | Static ranges for staging and production, available from your FourKites Implementation or Customer Success Manager |

## Common events

FourKites's Callbacks Data Dictionary Table lists its recommended callbacks:

| Callback | `MessageType` |
| --- | --- |
| Load Creation | `LOAD_CREATION` |
| Location Update | `LOCATION_UPDATE` |
| Tracking Update | `TRACKING_UPDATE` |
| Stop ETA Updated | `STOP_ETA_UPDATED` |
| Stop Arrival | `STOP_ARRIVAL` |
| Stop Departure | `STOP_DEPARTURE` |
| Stop Completion | `STOP_AUTO_COMPLETED`, `STOP_AUTO_DELIVERED`, `STOP_AUTO_PICKED_UP`, `MANUALLY_COMPLETED`, `MANUALLY_DELIVERED`, `MANUALLY_PICKED_UP` |
| Stop File Upload | `STOP_FILE_UPLOAD` |

Note that Stop Completion has six `MessageType` values, depending on whether the stop was completed automatically or manually and whether it was a pickup, a delivery or neither. Route on all six. Other callback pages, including the Network Visibility and appointment callbacks, document more message types, so treat this list as a starting point.

Each callback is a flat JSON object. Here is FourKites's `LOAD_CREATION` example, trimmed:

```json
{
  "MessageType": "LOAD_CREATION",
  "IsSuccess": true,
  "Errors": [],
  "FourKitesLoadId": 123654314,
  "LoadNumber": "2DB85AEB",
  "ProNumber": "4638",
  "Scac": "CC79",
  "Shipper": "cytc-company-new",
  "ReferenceNumbers": ["TU-2945", "80982", "94BC8"],
  "Tags": ["temp", "field2", "Inbound"],
  "EncryptedUrl": "https://app.fourkites.com/beta/eurl/...",
  "Timestamp": "2021-06-03T17:08:33+00:00",
  "Timezone": "Etc/UTC",
  "TimezoneOffset": 0,
  "TimezoneShortName": "UTC"
}

```

A few fields need care:

* `IsSuccess` and `Errors` tell you whether load creation actually worked. When it failed, there's no `FourKitesLoadId`.
* `LoadNumber` is your own identifier, supplied when you created the load. `FourKitesLoadId` is FourKites's.
* Stop timestamps are in the stop's local time with a UTC offset, and `TimezoneOffset` is in seconds.
* On `STOP_FILE_UPLOAD`, `FileUrl` is only valid for 24 hours, and FourKites advises downloading the file as soon as the message arrives.
* `EncryptedUrl` is a shared shipment status page that needs no authentication. Treat it as sensitive.

## Setting up FourKites webhooks

1. Choose a username and a long random password, for example with `openssl rand -hex 32`.
2. Ask your FourKites contact to subscribe your HTTPS endpoint to the callback messages you want, with **Basic** as the authentication method and your username and password.
3. Ask for the static IP ranges for staging and production if you want to restrict traffic by IP.

**In Hookdeck.** Create a source with the FourKites type and enter the same username and password. Give FourKites the source URL as the callback URL. Hookdeck reads the `Authorization: Basic` header and compares both values.

FourKites's docs don't say whether the Basic credentials are sent with the first request or only after a server answers `401` with a challenge. Hookdeck never sends a challenge, so FourKites has to send the credentials preemptively, on the first request. Confirm this with FourKites when you subscribe and check the first callbacks in Hookdeck. If they show as failed verification, ask FourKites to send the header through the subscription's **Additional Headers** instead, as `Authorization` with the value `Basic ` followed by the base64 of `username:password`.

The FourKites source type only checks Basic credentials. If your subscription uses API Key / Auth Header, use a Hookdeck source with API key verification instead. OAuth2 has FourKites fetch a token from a server you run, which a fixed check can't verify.

## Securing FourKites webhooks

Decode the `Authorization` header, split it on the first colon, and compare the username and password with your stored values in constant time.

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

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header, user, pass) {
  if (!user || !pass) return false; // fail closed when unconfigured
  const match = /^basic\s+(\S+)\s*$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64').toString('utf8');
  const i = decoded.indexOf(':'); // the password may contain ':'
  if (i === -1) return false;
  const userOk = safeEqual(decoded.slice(0, i), user);
  const passOk = safeEqual(decoded.slice(i + 1), pass);
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/fourkites', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBasicAuth(
    req.get('authorization'),
    process.env.FOURKITES_WEBHOOK_USERNAME,
    process.env.FOURKITES_WEBHOOK_PASSWORD
  );
  // Returning 401 here means FourKites won't retry: 4xx responses are final.
  if (!ok) return res.sendStatus(401);

  let message;
  try {
    message = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  // Route on message.MessageType, deduplicate, process async.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import re

def verify_basic_auth(header, user, password) -> bool:
    if not user or not password:
        return False  # fail closed
    match = re.fullmatch(r"(?i)basic\s+(\S+)\s*", header or "")
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True)
    except (binascii.Error, ValueError):
        return False
    received_user, sep, received_pass = decoded.partition(b":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(received_user, user.encode("utf-8"))
    pass_ok = hmac.compare_digest(received_pass, password.encode("utf-8"))
    return user_ok and pass_ok

```

Basic credentials prove the sender knows them. They don't prove the body is unchanged, and a captured request can be replayed and still pass, because FourKites sends no signature and no timestamp. Use HTTPS only, keep the password long and random, and restrict traffic to FourKites's IP ranges as a second layer.

> **Make FourKites callbacks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials on every callback, accepts each one straight away, and queues it durably with retries and replay.

## FourKites webhook limitations and pain points

### A `4xx` is final

**The Problem:** A bug that makes your handler return `400` or `422`, or a credentials mistake that returns `401`, loses every callback it touches.

**Why It Happens:** FourKites retries only `5xx` responses. "For any `4XX` code responses, retries will not be applied."

**Workarounds:**

* Return `5xx` for problems on your side that a retry could fix, such as a database outage, and keep `4xx` for requests that are genuinely bad.
* Don't let parsing or validation bugs surface as `4xx` before the event is stored. Store first, then validate.

**How Hookdeck Can Help:** Hookdeck accepts each callback as soon as it arrives, so FourKites always sees success, and then delivers it to your service with its own retries. A bug in your handler becomes a failed delivery you can replay after the fix, not a lost callback.

### Four retries, and nobody watching

**The Problem:** An outage longer than about a day loses data, and nothing tells you.

**Why It Happens:** FourKites retries `5xx` responses after 1 minute, 15 minutes, 1 hour and 24 hours, then stops. It also says it "does not actively monitor success/failure of HTTP responses from customer endpoints", and recommends that you log and monitor what you receive.

**Workarounds:**

* Monitor callback volume per `MessageType` and alert when it drops.
* After an incident, reconcile active loads through the FourKites API.

**How Hookdeck Can Help:** Hookdeck logs every callback it receives and every delivery to your service, and [issues](/docs/issues) alert you when deliveries fail, so the monitoring FourKites asks for comes built in.

### Basic credentials, with an unknown handshake

**The Problem:** Callbacks fail verification even though the username and password are right.

**Why It Happens:** FourKites doesn't document whether it sends Basic credentials on the first request. A client that waits for a `401` challenge never authenticates against an endpoint that doesn't send one, and Hookdeck doesn't.

**Workarounds:**

* Confirm with FourKites that credentials are sent preemptively.
* If they aren't, have FourKites send the `Authorization` header through **Additional Headers**.

**How Hookdeck Can Help:** Hookdeck's FourKites source type checks the Basic username and password on every request. Hookdeck answers FourKites with a `200` either way, then records a request with missing or wrong credentials as failed verification, so it never reaches your destination, and you can see straight away whether credentials are arriving.

### Proof-of-delivery links expire

**The Problem:** A POD file you try to fetch later returns an error.

**Why It Happens:** The `FileUrl` in `STOP_FILE_UPLOAD` is only active for 24 hours.

**Workarounds:**

* Download the file in a background job as soon as the callback arrives, and store it yourself.
* Alert on download failures, because the window to retry is short.

**How Hookdeck Can Help:** Because Hookdeck delivers each callback to your service promptly and retries failures quickly, your download job gets the URL while it's still valid, even if your service was briefly down when FourKites sent it.

## Best practices

### Route on `MessageType`, including all six completion types

Switch on `MessageType`, and make sure Stop Completion handling covers both the automatic and manual variants. Acknowledge types you don't recognise.

### Deduplicate deliveries

FourKites payloads have no delivery ID. Build a key from `MessageType`, `FourKitesLoadId` (or `LoadNumber`), the stop identifier where there is one, and `Timestamp`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Expect every key, with empty values

FourKites says every key is always present, with `null` or `[]` when an optional value is empty. Check values, not the presence of keys.

### Acknowledge fast and process asynchronously

Store the callback, return `200`, and do the work, such as POD downloads and TMS updates, in a background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Mind time zones

Stop times are local to the stop. Convert with the offset before comparing events from different stops.

## Conclusion

FourKites sends a JSON `POST` for each callback message you subscribe to, protected by Basic credentials rather than a signature. Check those credentials in constant time over HTTPS, make sure FourKites sends them on the first request, route on `MessageType`, download POD files within 24 hours, and never answer a recoverable problem with a `4xx`, because FourKites won't retry it.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the Basic credentials, accepts every callback immediately, and keeps it until your service has processed it.

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