Authentication

All integration endpoints use the same JWT bearer token you get from a standard WardenIQ dashboard sign-in. The token carries your tenant ID, user ID, and role, so every API call is automatically scoped to your tenant's data.

Two layers of security

Integration endpoints require both:

  1. A service token (long-lived JWT, machine-to-machine)
  2. An HMAC-SHA256 signature of the request body

The service token identifies which tenant is calling. The signature proves the request wasn't tampered with in transit and blocks replay attacks. A leaked service token on its own is not sufficient to write to your account — the attacker also needs the signing secret, which is never sent over the wire.

Step 1: Generate a service token

Service tokens are long-lived (1 year by default) and tied to a specific name for audit purposes. They are issued by an admin or owner on your tenant.

Using your admin dashboard JWT (obtained by signing in at wardeniq.com/dashboard and reading localStorage.quoteiq_token), call:

curl -X POST https://wardeniq.com/api/integrations/tokens \
  -H "Authorization: Bearer YOUR_ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "tms-production"}'

The response returns the plaintext token once. Save it immediately — it cannot be retrieved again.

{
  "id": 1,
  "name": "tms-production",
  "scope": "integrations:write",
  "expires_at": "2027-04-14T18:00:00Z",
  "token": "eyJ0eXAiOiJKV1Qi...",
  "warning": "Save this token now. It will not be shown again."
}

Step 2: Generate an HMAC signing secret

One-time setup. Run once per tenant for the TMS integration:

curl -X POST https://wardeniq.com/api/integrations/secrets \
  -H "Authorization: Bearer YOUR_ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "tms"}'

Response (plaintext secret returned once):

{
  "id": 1,
  "name": "tms",
  "secret": "k3z7Pq2A9fL5wR8vT1mN6yB4hJ0cX...",
  "warning": "Save this secret now. It will not be shown again."
}

Step 3: Sign and send each request

For every API call, compute the signature over {unix_timestamp}.{request_body} using HMAC-SHA256 with your signing secret, then send these headers:

Authorization: Bearer <SERVICE_TOKEN>
X-WardenIQ-Timestamp: <unix_epoch_seconds>
X-WardenIQ-Signature: sha256=<hex_digest>
Content-Type: application/json
The timestamp must be within 5 minutes of the server's clock. This is the replay protection window. If your system clock drifts significantly, synchronize it against NTP before sending requests.

Full curl example (bash)

#!/bin/bash
SERVICE_TOKEN="eyJ0eXAiOiJKV1Qi..."
SECRET="k3z7Pq2A9fL5wR8vT1mN6yB4hJ0cX..."
BODY='{"lane_ref":"1041-030-025","truck_cost":2250,"carrier_name":"Best Freight Inc","carrier_mc":"MC-123456"}'
TS=$(date +%s)
SIGNED_PAYLOAD="${TS}.${BODY}"
SIG=$(printf "%s" "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST https://wardeniq.com/api/integrations/tms/shipment-update \
  -H "Authorization: Bearer $SERVICE_TOKEN" \
  -H "X-WardenIQ-Timestamp: $TS" \
  -H "X-WardenIQ-Signature: sha256=$SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Example in Python

import hmac, hashlib, time, json, requests

SERVICE_TOKEN = "eyJ0eXAiOiJKV1Qi..."
SECRET = "k3z7Pq2A9fL5wR8vT1mN6yB4hJ0cX..."

body = json.dumps({
    "lane_ref": "1041-030-025",
    "truck_cost": 2250,
    "carrier_name": "Best Freight Inc",
    "carrier_mc": "MC-123456",
}, separators=(',', ':'))  # compact — same bytes server will verify

ts = str(int(time.time()))
signed = f"{ts}.{body}".encode()
sig = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()

r = requests.post(
    "https://wardeniq.com/api/integrations/tms/shipment-update",
    headers={
        "Authorization": f"Bearer {SERVICE_TOKEN}",
        "X-WardenIQ-Timestamp": ts,
        "X-WardenIQ-Signature": f"sha256={sig}",
        "Content-Type": "application/json",
    },
    data=body,  # NOT json= — we need the exact bytes we signed
)
print(r.status_code, r.json())
Important: sign the exact bytes you're sending. If you call requests.post(url, json=payload), requests will re-serialize the JSON and the bytes won't match your signature. Always pre-serialize to a string, hash that string, and send the same string as data=.

Error responses

Rate limiting

Integration endpoints are rate-limited to 10 requests per second per tenant. If your TMS pushes updates in bursts, add a small backoff on 429 Too Many Requests responses.

Security notes