Control Tower Data API

Read planned routes, live execution status, deviations and proof of delivery from ORTEC Control Tower — and get pushed webhook events as your routes are executed.

Getting started

Three steps: get credentials, exchange them for a token, call the API.

Base URL  https://integration-[customer].ortec-xs.com/api/v1
Authentication  OAuth 2.0 client credentials · Bearer token
Format  JSON · all timestamps ISO 8601 UTC

1 · Get credentials

Your ORTEC contact provides a client_id and client_secret scoped to your organization. The API only ever returns your own organization's data.

2 · Request an access token

curl -s https://integration-[customer].ortec-xs.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

The response contains an access_token and its lifetime in seconds (expires_in). Cache the token and request a new one shortly before it expires — do not request a token per API call.

3 · Call the API

curl -s "https://integration-[customer].ortec-xs.com/api/v1/routes?date=2026-08-21&detail=full" \
  -H "Authorization: Bearer $TOKEN"

Every route carries the plan (stops, time windows, order lines) and — once a driver has executed it — the realization: actual arrival and departure times, delivered amounts, deviations and proof-of-delivery attachments.

The data model in one minute

ResourceWhat it is
GET /departmentsThe distribution centers / depots you have access to.
GET /routesRoutes (trips). Filter by date, from/till, department, status, updatedSince. Use detail=summary (default) or detail=full.
GET /routes/{routeId}One route, always full detail: stops, orders, order lines, planned vs realized.
GET /routes/{routeId}/stops/{stopId}/attachmentsProof-of-delivery metadata for a stop (signatures, photos).
GET /attachments/{attachmentId}Downloads the actual file (image or PDF). Follow the href the API gives you.

Statuses

Route plannedactivecompleted

Stop plannedarrivedcompleted · deviations · cancelled

A stop ends in completed only when every order was executed exactly as planned. If anything differs — short delivery, refused items, extra amounts — the stop is deviations and each order line carries planned vs realized amounts so you can see precisely what changed.

Staying in sync

Poll for changes with updatedSince, or let us push webhooks — most integrations use both.

Delta polling

Store the timestamp of your last sync and ask only for what changed since:

GET /api/v1/routes?updatedSince=2026-08-21T06:00:00Z&detail=full&limit=100

Responses are paginated with a cursor. When pagination.nextCursor is present, repeat the request with &cursor=… until it is absent, then persist the newest updatedAt you saw as the next updatedSince.

Webhooks

Get an HTTPS call from us the moment something happens, instead of polling for it.

Subscribe

curl -s -X POST https://integration-[customer].ortec-xs.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-system.example.com/ct-events",
    "events": ["route.started", "route.completed", "stop.completed",
               "stop.deviation", "stop.cancelled", "attachment.created"]
  }'

The response contains the subscription and — only once — its signing secret. Store it. Use POST /webhooks/{id}/ping to send yourself a test event at any time.

Events

EventFired when
route.startedA driver starts executing a route.
route.completedA route is finished.
stop.arrivedThe driver arrives at a stop.
stop.completedA stop is executed exactly as planned.
stop.deviationA stop is executed with differences from the plan.
stop.cancelledA stop is not executed.
attachment.createdProof of delivery (signature, photo) becomes available.

Verify the signature

Every delivery carries X-CT-Event-Id (deduplicate on this) and X-CT-Signature: t=<unix seconds>,v1=<hex>. Recompute the HMAC over t + "." + rawBody with your subscription secret and compare:

const crypto = require('crypto');

function verify(signatureHeader, rawBody, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(kv => kv.split('=')));
  const expected = crypto.createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(parts.v1));
}

Respond with any 2xx within 10 seconds. Failed deliveries are retried with backoff (after 1, 5, 30, 120 and 360 minutes) for up to 48 hours; an endpoint that keeps failing for 3 days is deactivated and can be restored with a successful ping.

Errors & limits

Errors are JSON: {"error": {"code": "...", "message": "..."}} with conventional HTTP status codes (401 invalid token, 404 unknown resource, 422 invalid parameters).

Rate limit: 60 requests per minute per client. Beyond that you receive 429 with a Retry-After header — back off and retry. Webhooks and updatedSince polling keep you well under the limit.