REST API ยท /api/v1

Dock Optimizer REST API

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.

GET https://dockoptimizer.com/api/v1/...

๐Ÿ“– Overview

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.

๐Ÿ”’
Requires a tenant API key. Same rate-limit bucket as MCP. Audit entries are stored with a REST_GET method prefix so they're distinguishable from JSON-RPC entries in AgentAuditLogs.

๐Ÿ” Authentication

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}'
๐Ÿ”’
Security: API keys are SHA-256 hashed before storage. The raw token is shown exactly once during generation โ€” store it securely. Tokens begin with do_live_.

Key Scopes

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.

ScopeAllows
readresources/read on tenant URIs and all GET /api/v1/* endpoints. Discovery methods (tools/list, resources/list) do not require any scope.
writetools/call (auto-confirm and HITL tools) plus POST /api/mcp/v1/files/{eventId} multipart uploads. Implies read.
inventoryRequired in addition to write for receive_inventory, issue_inventory, adjust_inventory. Combine as read,write,inventory.
โ„น๏ธ
Keys with no scopes set have full access for backward compatibility. All new keys should be created with explicit scopes โ€” use read only for monitoring/observability agents, and read,write for agents that need to create or modify records.

Capability Gating (Fine-Grained Access)

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.

CapabilityGates
inventoryreceive_inventory, issue_inventory, adjust_inventory
appointmentsReserved โ€” 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"
}
โ„น๏ธ
A missing or empty capabilities array means all capability-gated tools are allowed (backward compatible). Explicit arrays are the recommended default going forward.

Environment Tagging

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.

โš ๏ธ
Generate each key in the environment you plan to use it. The Integration Hub only lets you mint keys for the env it itself is running in โ€” a prod key must be generated in prod, a staging key in staging.

Key Identity & Metadata

Every key carries identifying metadata so admins can distinguish one agent from another on the dashboard and in audit logs:

FieldPurpose
agentNameShort identifier โ€” e.g. zo-booking-prod, claude-desktop-akash
contactEmailOwner email; receives alerts on suspicious use or quota exhaustion
descriptionFree-text purpose โ€” what the agent is for, where it runs
expiresAtOptional absolute expiry (UTC). Recommended for experimental or user-BYO keys
writeRateLimitPer-hour ceiling on write-tool calls (null = inherit the daily quota only)

Rotation & Revocation

๐Ÿ“œ OpenAPI 3 Spec + Explorer

Partners generating typed SDKs can point their codegen at the machine-readable spec; everyone else can poke at endpoints interactively in the Swagger UI.

ResourceURL
OpenAPI 3.0 JSON/api/v1/swagger/v1/swagger.json
Interactive explorer (Swagger UI)/api/docs/explorer โ†—
Run in PostmanIn 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.

๐Ÿ›ฃ๏ธ Endpoints

All paths return application/json. Filter, sort, and paginate via query parameters; combine with If-None-Match for cheap polling.

MethodPathPurpose
GET/api/v1/appointmentsList with filters (start, end, date, status, facilityId, carrier, search, limit, offset, sortBy)
GET/api/v1/appointments/changedDelta 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/checkConflict / duplicate check (date, facilityId, carrierName, bolNumber)
GET/api/v1/facilitiesAll facilities with doors, timezone, address
GET/api/v1/event-typesAll appointment types
GET/api/v1/slots/availableAvailable slots (date, optional facilityId)
GET/api/v1/carriers/searchFuzzy carrier search (name, mc)
GET/api/v1/operations/todayPer-facility dashboard (optional date)
GET/api/v1/loadsLoads you posted or bid on (status, role=shipper|carrier, internal, limit, offset). Requires the Load Board feature.
GET/api/v1/loads/changedLoad 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/bidsBids received on your loads + bids you placed (loadId, status, limit, offset)
GET/api/v1/rate-confirmationsRate confirmations you're party to (loadId, status, limit, offset)

Response Headers

HeaderAppears onPurpose
X-Request-Idevery responseServer-generated UUID (or the one you supplied inbound). Quote it in support tickets.
Link (RFC 8288)/appointments, /appointments/changed, /loads, /loads/changed, /bids, /rate-confirmationsrel="next", rel="prev", rel="first", rel="last", rel="self" for pagination.
X-Total-Count/appointments, /loads, /bids, /rate-confirmationsTotal records matched across all pages.
ETag/facilities, /event-types, /loads, /bids, /rate-confirmationsWeak ETag. Send back as If-None-Match on your next poll to get a 304.

Example

curl -H "Authorization: Bearer do_live_..." \
  "https://dockoptimizer.com/api/v1/appointments?date=2026-04-22&facilityId=18"

Polling with ETags

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.

Delta sync

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.

Freight visibility

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.

โšก Outbound webhooks

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.

HeaderPurpose
X-DockOptimizer-EventThe event name, e.g. load.awarded.
X-DockOptimizer-DeliveryUnique delivery id โ€” deduplicate retries on this.
X-DockOptimizer-Signaturesha256=<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.

๐Ÿ”„ Partner TMS sync recipe

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.

1 ยท Backfill โ€” page through everything once

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.

2 ยท Stay current โ€” webhooks + the delta 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.

3 ยท Verify webhook signatures

// 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));
}

4 ยท Generate a typed client

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).

โœ๏ธ Writes? Use MCP

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.

โ„น๏ธ
The same Bearer token works for both surfaces. Add scopes: "read,write" when generating the key, then call POST /api/mcp/v1/message with a tools/call payload. See the MCP tools reference.

โŒ Error Handling

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.

RFC 7807 Problem Details (REST + transport-level MCP errors)

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"
}

JSON-RPC error codes (MCP)

HTTPJSON-RPC CodeMeaning
400-32700Parse error โ€” invalid JSON in request body
401โ€”Missing, invalid, or expired API key
403-32600Insufficient scope for this operation
400-32601Method not found
400-32602Invalid params โ€” missing required fields
404-32001Invalid resource URI format
404-32002Resource not found or empty result
429โ€”Rate limit exceeded โ€” implement exponential backoff
500-32603Internal server error

Recovery Hints

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."
    }
  }
}

โšก Rate Limits

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.

LimitWindowScopeConfigurable
60 requestsPer minutePer API key (burst)Global โ€” rejection is immediate (no queue)
1,000 requestsPer UTC dayPer API key (quota)Server-wide via MCP:DailyCallLimit
Write-tool ceilingPer hourPer API key (optional)Per-key via update-metadata (null = unlimited)

429 Response Headers

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.

โš ๏ธ
Backoff strategy for agents: on 429, wait 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.

Right-sizing Per-Key Limits

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.

๐Ÿ”‘ Key Management

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.

GET /api/agent-keys/list

List all active keys for the calling admin's organization with agent name, prefix, scopes, lastUsedAt, daily/total call counts, and expiration status.

GET /api/agent-keys/dashboard

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.

POST /api/agent-keys/generate

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.

POST /api/agent-keys/{id}/regenerate

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.

POST /api/agent-keys/{id}/revoke

Revoke an API key immediately โ€” all future requests with this key will return 401. Irreversible.

POST /api/agent-keys/{id}/update-metadata

Update owner-visible metadata. Body accepts contactEmail, description, agentConfig (validated JSON), and writeRateLimit. Scopes and expiration are immutable โ€” rotate the key to change them.

GET /api/agent-keys/activity/{id}?limit=20

Return the most recent audit-log entries for a single key: method, target, response code, duration, timestamp. Capped at 100 per request.