Skip to Content
FR8 Factory Docs

Signing

HMAC-SHA256 over the raw body, with a worked example you can reproduce before writing code.

Every delivery carries a signature and a timestamp in one header:

X-LDI-Signature: t=1758637862,v1=54f311486063ca1dcb3a21d2831a698ab45b59b58732e4d32df9f9aacb0989f6
  • t — Unix seconds at send time
  • v1 — lowercase hex HMAC-SHA256

Send exactly one v1= value. The format this borrows from permits several; we do not.

How to compute it

The signed string is the timestamp, a literal ., then the request body:

signed_payload = t + "." + raw_request_body
signature      = hex(hmac_sha256(shared_secret, signed_payload))

Sign the exact bytes you put on the wire

This is where HMAC integrations actually break, and the failure is opaque from both ends.

If the payload is serialized once for signing and again for sending, any difference in key order or whitespace between the two changes the signature. Every delivery then fails verification, and neither side can see why from the logs.

Serialize once, sign that string, send that string.

This is the same scheme Stripe uses. Deliberately so — it is widely implemented, there are reference implementations in every language, and it is probably already familiar to whoever is reading this.

Why the timestamp is in the signature

A bare signature is replayable forever by anyone who captures one delivery. Including t in the signed payload, and checking it on receipt, bounds that window.

We reject any request where |now − t| exceeds five minutes.

That has one consequence for retries: t is stamped when an attempt is sent, not when the event occurred, so every attempt is signed afresh. See Delivery.

Worked example

Reproduce this before writing the sender. If these inputs produce this signature, the implementation is correct.

Example key only

The secret below is an illustration, not a real credential. Yours is exchanged out of band.

Secret

whsec_3f8a1c94d2e64b07a5c18e2f9b03d7a6

Timestamp

1758637862

Body — exactly 154 bytes, no trailing newline

{"eventId":"9f2c1a84-3d7e-4b19-a0f5-1c2e6b8d4a30","eventType":"load.created","occurredAt":"2026-09-23T14:31:02Z","apiVersion":1,"data":{"loadId":2129858}}

Signed payload — t, a dot, then that body

1758637862.{"eventId":"9f2c1a84-3d7e-4b19-a0f5-1c2e6b8d4a30","eventType":"load.created","occurredAt":"2026-09-23T14:31:02Z","apiVersion":1,"data":{"loadId":2129858}}

Expected signature

54f311486063ca1dcb3a21d2831a698ab45b59b58732e4d32df9f9aacb0989f6

Resulting header

X-LDI-Signature: t=1758637862,v1=54f311486063ca1dcb3a21d2831a698ab45b59b58732e4d32df9f9aacb0989f6

Check it from a shell

printf '1758637862.{"eventId":"9f2c1a84-3d7e-4b19-a0f5-1c2e6b8d4a30","eventType":"load.created","occurredAt":"2026-09-23T14:31:02Z","apiVersion":1,"data":{"loadId":2129858}}' \
  | openssl dgst -sha256 -hmac 'whsec_3f8a1c94d2e64b07a5c18e2f9b03d7a6'

Secrets and rotation

Secrets are 32+ random bytes, one per environment, exchanged out of band. They never appear in these docs.

We accept two valid secrets during a rotation window — a delivery matching either is accepted. So rotation is:

  1. we add the new secret
  2. you switch to it whenever suits you
  3. we drop the old one

No coordinated cutover, no maintenance window, no dropped events. The rotation is ours to absorb rather than yours to schedule.

On this page

Help

How do I get API access?

API keys are issued per customer by the FR8 Factory team. Ask your account contact — or send us a request through the Help desk tab with your company name and use case.

How do I authenticate requests?

Send your API key in the Authorization header on every request. Keys are secret — never ship them in browser code or mobile apps.

Open the guide — How do I authenticate requests?

How do I keep my copy of the data in sync?

Use delta polling: request only records changed since your last sync cursor instead of re-downloading everything. The guide covers cursors, ordering, and replay safety.

Open the guide — How do I keep my copy of the data in sync?

What are the rate limits?

Limits are per API key and documented on the rate-limits page, along with the response headers that tell you where you stand and how to back off.

Open the guide — What are the rate limits?

What does each load field mean?

The load field dictionary defines every field the API returns — types, nullability, and freight-domain meaning (pick vs drop dates, AR, equipment types).

Open the guide — What does each load field mean?

Found a bug or missing data?

Send it through the Help desk tab with the request you made and what you expected back. You'll get a reference code, and we'll email you when it's resolved.