Getting started
Three steps: get credentials, exchange them for a token, call the API.
https://integration-[customer].ortec-xs.com/api/v1Authentication 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
| Resource | What it is |
|---|---|
GET /departments | The distribution centers / depots you have access to. |
GET /routes | Routes (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}/attachments | Proof-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 planned → active → completed
Stop planned → arrived → completed · 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
| Event | Fired when |
|---|---|
route.started | A driver starts executing a route. |
route.completed | A route is finished. |
stop.arrived | The driver arrives at a stop. |
stop.completed | A stop is executed exactly as planned. |
stop.deviation | A stop is executed with differences from the plan. |
stop.cancelled | A stop is not executed. |
attachment.created | Proof 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.