Wripp IQ for developers
Connect your TMS, ERP or AI agent to the Wripp IQ freight marketplace. Post loads, search open freight, place bids and get signed webhooks, with the same rules as the web app.
Overview
Base URL: https://api-production-dd08.up.railway.app/api. The write API lives under /v1; read-only feeds live under /public. Requests and responses are JSON.
API keys belong to an organization. An owner or admin creates them in Settings → Developer, where you also register webhook endpoints and send test events.
Authentication & scopes
Send an organization API key on every request:
x-api-key: wrpk_<prefix>_<secret>Authorization: Bearer wrpk_… also works. Every call acts as the organization that owns the key.
Managing keys
Keys are managed by a signed-in user whose org role has manage_team (owner or admin). These routes use the normal web session, not an API key.
| Method | Path | Notes |
|---|---|---|
GET | /api/developer/keys | Lists keys (masked), with scopes, limit, and rotation state |
POST | /api/developer/keys | { name, scopes?, rateLimitPerMinute? } → { id, name, key, scopes, rateLimitPerMinute }. The secret is shown once. |
POST | /api/developer/keys/:id/rotate | → { id, name, key, previousExpiresAt } |
DELETE | /api/developer/keys/:id | Revokes the key immediately |
Rotation. Rotating issues a new secret under the same prefix. The previous secret keeps working for 24 hours (previousExpiresAt), so you can roll deployments over. If you rotate again inside that window, the older secret stops working immediately. Only the current secret and the one before it are ever valid.
Every create, rotate and revoke is written to the org security log.
Choose scopes when you create a key. If you omit scopes, the key gets loads:read, bookings:read and invoices:read. Keys created before scopes existed were backfilled to that same set.
| Scope | Grants |
|---|---|
loads:read | GET /v1/loads/search, GET /v1/loads/:id, GET /public/loads |
loads:write | POST /v1/loads |
bids:read | GET /v1/bids |
bids:write | POST /v1/loads/:loadId/bids |
market:read | GET /v1/market-range, GET /v1/loads/search, GET /v1/market/index (ELITE+ plan) |
bookings:read | GET /public/bookings |
invoices:read | Reserved for GET /public/invoices (that route currently accepts any valid key; ELITE+ plan) |
bids:autonomous | Lets a key place bids without per-bid human approval. You can't grant this yourself. Wripp staff grant it per organization after review. |
A request whose key lacks the scope returns 403 with code INSUFFICIENT_SCOPE.
Quickstarts
Set WRIPP=https://api-production-dd08.up.railway.app/api and WRIPP_API_KEY first. Node examples use the built-in fetch (Node 18+).
Post a load loads:write
curl -X POST "$WRIPP/v1/loads" \
-H "x-api-key: $WRIPP_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" \
-d '{
"title": "Steel coils Dallas → Atlanta", "commodity": "Steel Coils", "equipmentType": "FLATBED",
"weightValue": 42000, "weightUnit": "LBS", "accuracyCertified": true,
"pickupWindowStart": "2026-09-20T15:00:00Z",
"stops": [
{ "stopType": "PICKUP", "city": "Dallas", "state": "TX", "postalCode": "75201" },
{ "stopType": "DROPOFF", "city": "Atlanta", "state": "GA", "postalCode": "30303" }
]
}'const res = await fetch(`${process.env.WRIPP}/v1/loads`, {
method: 'POST',
headers: { 'x-api-key': process.env.WRIPP_API_KEY, 'Idempotency-Key': crypto.randomUUID(), 'content-type': 'application/json' },
body: JSON.stringify({
title: 'Steel coils Dallas → Atlanta', commodity: 'Steel Coils', equipmentType: 'FLATBED',
weightValue: 42000, weightUnit: 'LBS', accuracyCertified: true, pickupWindowStart: '2026-09-20T15:00:00Z',
stops: [
{ stopType: 'PICKUP', city: 'Dallas', state: 'TX', postalCode: '75201' },
{ stopType: 'DROPOFF', city: 'Atlanta', state: 'GA', postalCode: '30303' },
],
}),
})
const { data: load } = await res.json() // load.status is POSTEDSearch loads loads:read
curl "$WRIPP/v1/loads/search?originState=TX&equipmentType=REEFER&limit=20" -H "x-api-key: $WRIPP_API_KEY"const qs = new URLSearchParams({ originState: 'TX', equipmentType: 'REEFER', limit: '20' })
const res = await fetch(`${process.env.WRIPP}/v1/loads/search?${qs}`, { headers: { 'x-api-key': process.env.WRIPP_API_KEY } })
const { data: loads, meta } = await res.json()Place a bid bids:write
curl -X POST "$WRIPP/v1/loads/$LOAD_ID/bids" \
-H "x-api-key: $WRIPP_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" \
-d '{ "bidAmount": 2350, "transitEtaText": "Next-day delivery",
"attestation": { "agent": "acme-dispatch/2.1", "humanApproved": true } }'const res = await fetch(`${process.env.WRIPP}/v1/loads/${loadId}/bids`, {
method: 'POST',
headers: { 'x-api-key': process.env.WRIPP_API_KEY, 'Idempotency-Key': crypto.randomUUID(), 'content-type': 'application/json' },
body: JSON.stringify({
bidAmount: 2350,
transitEtaText: 'Next-day delivery',
// humanApproved: true only when a person approved this bid (keys without bids:autonomous get 403 otherwise).
attestation: { agent: 'acme-dispatch/2.1', humanApproved: true },
}),
})Webhooks
Register an HTTPS endpoint in the dashboard. You get a whsec_… signing secret once. Every event is stored before it is sent and retried with backoff for about 1.8 days until your endpoint answers 2xx within 5 seconds. You can redeliver any delivery from the dashboard.
Each delivery is a POST with this body and headers:
POST /your/endpoint
Content-Type: application/json
Wripp-Signature: t=1789891200,v1=5f0c… (HMAC-SHA256 of "<t>.<raw body>" with your secret)
Wripp-Event-Id: 7d9f… (same as body.id; dedupe on it)
{ "id": "7d9f…", "event": "booking.status_changed", "data": { … }, "timestamp": "2026-09-20T15:00:00.000Z" }Verify the signature
Compute the HMAC over the raw body, compare in constant time, and reject timestamps older than 5 minutes.
const { createHmac, timingSafeEqual } = require('node:crypto')
// rawBody: the request body exactly as received (a string or Buffer — not re-serialized JSON).
// header: the Wripp-Signature header, "t=<unix seconds>,v1=<hex>". secret: your whsec_… signing secret.
function verifyWrippSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.split('=')))
const t = Number(parts.t)
if (!Number.isInteger(t) || !parts.v1) return false
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false // replayed or stale
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1)
return a.length === b.length && timingSafeEqual(a, b)
}Receive events
const express = require('express')
const app = express()
// Keep the raw body: the signature covers the exact bytes Wripp sent.
app.post('/wripp/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWrippSignature(req.body.toString('utf8'), req.get('Wripp-Signature'), process.env.WRIPP_WEBHOOK_SECRET)) {
return res.status(400).send('bad signature')
}
const event = JSON.parse(req.body)
// Deliveries can repeat (retries, redeliver): dedupe on event.id (also sent as Wripp-Event-Id).
console.log(event.event, event.id, event.data)
res.sendStatus(200) // any 2xx within 5 seconds counts as delivered
})
app.listen(3000)Event catalog
load.posted
A load matching one of your saved searches was posted to the marketplace. Sent to carrier orgs, once per load.
{
"loadId": "3f6c1f0e-8a52-4d0b-9d6e-2f1b8c7a9e10",
"title": "Steel coils Dallas → Atlanta"
}booking.status_changed
A booking moved to a new status (for example PICKED_UP, IN_TRANSIT, DELIVERED). Sent to both the shipper and the carrier org.
{
"bookingId": "9b2d7c4e-1f3a-4e8b-a6d5-0c9e8f7b6a51",
"status": "IN_TRANSIT",
"loadId": "3f6c1f0e-8a52-4d0b-9d6e-2f1b8c7a9e10"
}test.ping
A test event you sent from the developer dashboard. Delivered to the chosen endpoint whatever events it subscribes to.
{
"message": "Test event from Wripp IQ",
"webhookId": "c1a9e8d7-6b5f-4a3e-9d2c-1b0a9f8e7d6c"
}Endpoints subscribed to * receive every event. New event types may be added: ignore events you don't handle.
Rate limits, idempotency & versioning
Each key has its own limit in requests per minute. The limit follows the key across IPs and servers. The default is 60. You can set a higher limit when you create the key, up to your plan's ceiling:
| Plan | Max requests/min per key |
|---|---|
| FREE | 60 |
| STARTER | 120 |
| PRO | 300 |
| ELITE | 600 |
| ENTERPRISE | 1200 |
Responses include X-RateLimit-Limit and X-RateLimit-Remaining. When the limit is exceeded you get 429 with a Retry-After header (seconds). Plan limits for loads and bids still apply on top of the rate limit.
Idempotency
Every POST under /v1 requires an Idempotency-Key header of 1–255 characters, such as a UUID. Without one the request fails with 400 IDEMPOTENCY_KEY_REQUIRED.
- A successful response (status below 400) is stored for 24 hours. Sending the same key again returns the original status and body, with the header
Idempotent-Replayed: true, and nothing is written a second time. - Reusing a key for a different request (a different path or body) returns
422 IDEMPOTENCY_KEY_REUSED. - Failed responses are not stored, so you can retry a failed request with the same key.
- Stored responses belong to the secret that sent them. After a rotation, a retry sent with the new secret counts as a new request.
- Two identical requests sent at the same moment are not deduplicated. Retry sequentially.
Response envelope
Success responses (for /v1 and the public feed):
{ "data": { "...": "..." }, "meta": { "requestId": "6f1c…", "count": 20 } }count appears when data is a list. Some routes add more fields to meta, such as disclaimer or cacheSeconds.
Errors, on every route:
{ "error": { "code": "INSUFFICIENT_SCOPE", "message": "…", "statusCode": 403, "requestId": "6f1c…", "timestamp": "…", "path": "/api/v1/loads", "details": {} } }Always quote requestId when you contact support.
Versioning
The version is in the URL (/api/v1). Within v1 we only make additive changes: new endpoints, new optional fields, new webhook events. Ignore fields you don't recognize. A breaking change ships under a new version prefix.
MCP server for AI agents
The Wripp IQ Model Context Protocol server lets agents such as Claude work with the marketplace through the v1 API, using your API key and its scopes. It exposes search_loads, get_market_range, create_load and submit_bid.
Every programmatic bid needs a person's approval: submit_bid only runs with human_approved: true. Contact us to get the server.
