Read-only JSON over the same data MCP exposes โ built for partner TMS/ERP systems and Integration Hub connectors that don't speak JSON-RPC 2.0.
The REST surface is a thin, plain-JSON view over the same data the MCP server exposes. Every endpoint is GET (read-only); for writes, use the MCP tools/call surface. Same auth, same rate-limit bucket, same audit trail โ the two surfaces share one credential.
REST_GET method prefix so they're distinguishable from JSON-RPC entries in AgentAuditLogs.All API requests require a Bearer token in the Authorization header. API keys are organization-specific โ an admin in your organization generates them from the Integration Hub โ Agent Keys tab. Each key is scoped to that organization's data.
curl -X POST https://dockoptimizer.com/api/mcp/v1/message \ -H "Authorization: Bearer do_live_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
do_live_.
Scopes gate the write-capable RPC methods. Discovery (tools/list and resources/list) is always allowed so a client can introspect the server regardless of what it's permitted to do.
| Scope | Allows |
|---|---|
| read | resources/read on tenant URIs and all GET /api/v1/* endpoints. Discovery methods (tools/list, resources/list) do not require any scope. |
| write | tools/call (auto-confirm and HITL tools) plus POST /api/mcp/v1/files/{eventId} multipart uploads. Implies read. |
| inventory | Required in addition to write for receive_inventory, issue_inventory, adjust_inventory. Combine as read,write,inventory. |
read only for monitoring/observability agents, and read,write for agents that need to create or modify records.Scopes gate methods; capabilities gate individual tools. Sensitive tool families check the key's agentConfig.capabilities[] array and return SCOPE_DENIED if the capability is missing. Use this to keep a booking agent from touching stock, or an inventory agent from moving appointments.
| Capability | Gates |
|---|---|
inventory | receive_inventory, issue_inventory, adjust_inventory |
appointments | Reserved โ future appointment tool gating |
Configure per key via POST /api/agent-keys/{id}/update-metadata with an agentConfig payload:
{
"version": 1,
"capabilities": ["appointments", "inventory"],
"defaultFacilityId": 19,
"notes": "Zo booking agent, prod"
}
capabilities array means all capability-gated tools are allowed (backward compatible). Explicit arrays are the recommended default going forward.Keys may carry an environment tag of staging or production. The auth filter compares that tag against the server's ASPNETCORE_ENVIRONMENT on every request โ a tagged key whose environment doesn't match the server is rejected with 401. Tenant keys may leave the tag blank for legacy compatibility.
Every key carries identifying metadata so admins can distinguish one agent from another on the dashboard and in audit logs:
| Field | Purpose |
|---|---|
agentName | Short identifier โ e.g. zo-booking-prod, claude-desktop-akash |
contactEmail | Owner email; receives alerts on suspicious use or quota exhaustion |
description | Free-text purpose โ what the agent is for, where it runs |
expiresAt | Optional absolute expiry (UTC). Recommended for experimental or user-BYO keys |
writeRateLimit | Per-hour ceiling on write-tool calls (null = inherit the daily quota only) |
POST /api/agent-keys/{id}/regenerate to atomically revoke the old token and issue a new one with the same metadata. Recommended cadence: 90 days for production agents, 30 days for shared developer keys.POST /api/agent-keys/{id}/revoke takes effect on the next request.Partners generating typed SDKs can point their codegen at the machine-readable spec; everyone else can poke at endpoints interactively in the Swagger UI.
| Resource | URL |
|---|---|
| OpenAPI 3.0 JSON | /api/v1/swagger/v1/swagger.json |
| Interactive explorer (Swagger UI) | /api/docs/explorer โ |
| Run in Postman | In Postman, choose Import โ Link and paste the OpenAPI URL above (/api/v1/swagger/v1/swagger.json). Postman generates a collection with every endpoint, its query parameters, and the Bearer auth pre-wired โ just set your do_live_โฆ key. |
All paths return application/json. Filter, sort, and paginate via query parameters; combine with If-None-Match for cheap polling.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/appointments | List with filters (start, end, date, status, facilityId, carrier, search, limit, offset, sortBy) |
| GET | /api/v1/appointments/changed | Delta sync cursor (since, optional facilityId, limit) with nextSince in the response |
| GET | /api/v1/appointments/{id} | Full detail for one appointment |
| GET | /api/v1/appointments/check | Conflict / duplicate check (date, facilityId, carrierName, bolNumber) |
| GET | /api/v1/facilities | All facilities with doors, timezone, address |
| GET | /api/v1/event-types | All appointment types |
| GET | /api/v1/slots/available | Available slots (date, optional facilityId) |
| GET | /api/v1/carriers/search | Fuzzy carrier search (name, mc) |
| GET | /api/v1/operations/today | Per-facility dashboard (optional date) |
| GET | /api/v1/loads | Loads you posted or bid on (status, role=shipper|carrier, internal, limit, offset). Requires the Load Board feature. |
| GET | /api/v1/loads/changed | Load delta sync (since, limit) with nextSince cursor; includes soft-deleted rows |
| GET | /api/v1/loads/{id} | Full load detail with bids and rate-confirmation status |
| GET | /api/v1/bids | Bids received on your loads + bids you placed (loadId, status, limit, offset) |
| GET | /api/v1/rate-confirmations | Rate confirmations you're party to (loadId, status, limit, offset) |
| Header | Appears on | Purpose |
|---|---|---|
X-Request-Id | every response | Server-generated UUID (or the one you supplied inbound). Quote it in support tickets. |
Link (RFC 8288) | /appointments, /appointments/changed, /loads, /loads/changed, /bids, /rate-confirmations | rel="next", rel="prev", rel="first", rel="last", rel="self" for pagination. |
X-Total-Count | /appointments, /loads, /bids, /rate-confirmations | Total records matched across all pages. |
ETag | /facilities, /event-types, /loads, /bids, /rate-confirmations | Weak ETag. Send back as If-None-Match on your next poll to get a 304. |
curl -H "Authorization: Bearer do_live_..." \ "https://dockoptimizer.com/api/v1/appointments?date=2026-04-22&facilityId=18"
curl -H "Authorization: Bearer do_live_..." \ -H "If-None-Match: \"abc123\"" \ "https://dockoptimizer.com/api/v1/facilities" # Returns 304 Not Modified if the response would be identical.
For long-running sync loops, use /api/v1/appointments/changed?since=โฆ or /api/v1/loads/changed?since=โฆ with the nextSince cursor returned in the previous response. This avoids re-pulling unchanged records on every poll.
The load endpoints return exactly what your organization can see in the app: loads you posted (shipper side) plus loads your carrier account has bid on, including awarded freight (carrier side). Brokerage fields โ customerRate, customerName, margin โ appear only on your own loads when your org is on the Pro TMS tier and has declared itself a broker; carrierTakeHome appears only on your own bids.
Instead of polling /loads/changed, org admins can register webhook endpoints (Load Board โ Webhooks) and receive a signed POST on every load lifecycle transition: load.posted, load.awarded, load.in_transit, load.delivered, load.settled.
| Header | Purpose |
|---|---|
X-DockOptimizer-Event | The event name, e.g. load.awarded. |
X-DockOptimizer-Delivery | Unique delivery id โ deduplicate retries on this. |
X-DockOptimizer-Signature | sha256=<hex> โ HMAC-SHA256 of the raw request body keyed with your endpoint's signing secret. Verify before trusting the payload. |
Deliveries time out after 10 seconds and retry with backoff; respond 2xx quickly and process asynchronously. Endpoints that fail repeatedly are disabled automatically and surfaced on the management page.
The standard integration is a one-time backfill, then webhooks for real-time updates, with the delta cursor as the safety net for anything missed while your endpoint was down.
curl -H "Authorization: Bearer do_live_..." \ "https://dockoptimizer.com/api/v1/loads?limit=200&offset=0" # Follow rel="next" in the Link header until it disappears. # Record max(lastModified) across pages โ that's your starting cursor.
Register a webhook endpoint for real-time pushes. On every webhook you process โ and on a timer as a catch-up sweep โ advance the cursor:
curl -H "Authorization: Bearer do_live_..." \ "https://dockoptimizer.com/api/v1/loads/changed?since=2026-06-12T00:00:00Z" # Response carries nextSince โ persist it and pass it back on the next poll. # Rows include isDeleted so removals replicate too.
// Node.js โ same idea in any language: HMAC-SHA256 the RAW body.
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
.update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
npx @openapitools/openapi-generator-cli generate \ -i https://dockoptimizer.com/api/v1/swagger/v1/swagger.json \ -g typescript-fetch -o ./dock-optimizer-client
Bringing history FROM another TMS instead? Use the guided importer at /TmsImport (CSV/JSON, dry-run preview, idempotent re-runs).
The REST surface is intentionally read-only. All mutations โ creating appointments, recording inventory, blocking doors โ go through the MCP tools/call endpoint. This keeps the REST contract narrow and lets us add idempotency keys, HITL gating, and tool-level capability checks on writes without complicating the REST schema.
scopes: "read,write" when generating the key, then call POST /api/mcp/v1/message with a tools/call payload. See the MCP tools reference.The API uses standard JSON-RPC 2.0 error codes alongside HTTP status codes. Error responses include an optional data.hint field with recovery guidance for agents.
Unhandled errors on /api/v1/* and transport-level errors on /api/mcp/* come back as application/problem+json:
{
"type": "https://dockoptimizer.com/errors/internal",
"title": "Internal server error",
"status": 500,
"detail": "An unexpected error occurred. Our team has been notified.",
"instance": "/api/v1/appointments",
"requestId": "8f4d2c1a7b6e4e3f9a0c1b8d2e7f4a9c"
}
| HTTP | JSON-RPC Code | Meaning |
|---|---|---|
| 400 | -32700 | Parse error โ invalid JSON in request body |
| 401 | โ | Missing, invalid, or expired API key |
| 403 | -32600 | Insufficient scope for this operation |
| 400 | -32601 | Method not found |
| 400 | -32602 | Invalid params โ missing required fields |
| 404 | -32001 | Invalid resource URI format |
| 404 | -32002 | Resource not found or empty result |
| 429 | โ | Rate limit exceeded โ implement exponential backoff |
| 500 | -32603 | Internal server error |
Error responses may include a data.hint field with actionable guidance:
{
"error": {
"code": -32002,
"message": "Resource not found or empty result for URI: dock://appointments/99999",
"data": {
"hint": "The requested resource returned no data. Check the appointment ID or filters."
}
}
}
API requests are rate-limited per API key across three windows. The three layers protect against burst traffic, runaway loops, and accidental write storms from a single agent. REST and MCP traffic share the same bucket โ the limits below are per key, not per surface.
| Limit | Window | Scope | Configurable |
|---|---|---|---|
| 60 requests | Per minute | Per API key (burst) | Global โ rejection is immediate (no queue) |
| 1,000 requests | Per UTC day | Per API key (quota) | Server-wide via MCP:DailyCallLimit |
| Write-tool ceiling | Per hour | Per API key (optional) | Per-key via update-metadata (null = unlimited) |
When rate-limited, the API returns HTTP 429 Too Many Requests. The minute-window limiter is a token bucket โ the bucket refills continuously. The daily window resets at 00:00 UTC.
1s, 2s, 4s, 8s, 16sโฆ with jitter, capped at 60s. Don't retry the same tool call on 429 โ re-read the relevant resource first in case state has changed.Use writeRateLimit on keys that should only make occasional changes (a data-quality agent, a report runner). Leave it null for keys that may legitimately run bursts (a bulk backfill import). The per-minute burst limit still applies either way.
Manage API keys programmatically via the admin endpoints (requires an authenticated admin session cookie โ these endpoints are not callable from an agent using its own API key). The same keys work for both REST and MCP traffic.
List all active keys for the calling admin's organization with agent name, prefix, scopes, lastUsedAt, daily/total call counts, and expiration status.
Everything /list returns, plus per-key recent activity samples and aggregate stats (total keys, calls today, all-time calls, effective rate limits). Use this for the admin dashboard.
Generate a new API key. Body: { "agentName": "โฆ", "scopes": "read,write", "expiresAt": "2026-07-01T00:00:00Z" }. Returns the raw token exactly once โ store it immediately; it is not recoverable.
Atomically revoke an existing key and issue a new one carrying the same agentName, scopes, expiresAt, contactEmail, description, agentConfig, environment, and writeRateLimit. Use this for rotation without reconfiguring agent metadata.
Revoke an API key immediately โ all future requests with this key will return 401. Irreversible.
Update owner-visible metadata. Body accepts contactEmail, description, agentConfig (validated JSON), and writeRateLimit. Scopes and expiration are immutable โ rotate the key to change them.
Return the most recent audit-log entries for a single key: method, target, response code, duration, timestamp. Capped at 100 per request.