API documentation

The API runs the whole life of an order from your own ERP or TMS: create the order, choose an offer or assign a known vehicle, follow the trip with arrivals, stop confirmations and photos, amend the route in progress, rate the carrier, handle claims, and get a notification at your own URL whenever something changes. Responses and error codes are in English so your program can branch on them.

Machine-readable description (OpenAPI 3.1) for Postman, Insomnia or a client generator: https://www.rahtis.eu/api/v1/openapi.json

Keys and webhooks are created in the cabinet, on the API tab.

Getting started

Create a key in the cabinet. The key is shown once — store it in your secret manager. A read key is enough to follow orders; every change (creating, choosing, assigning, route changes, rating, claims) needs write access.

A key of a test company starts with rhs_test_ and only sees test orders; responses then carry Rahtis-Environment: test. A key of a live company starts with rhs_live_.

Send the key in every request as Authorization: Bearer.

curl https://www.rahtis.eu/api/v1/orders?status=OPEN,IN_PROGRESS \
  -H "Authorization: Bearer $RAHTIS_KEY"

Access, times and money

The API is for approved shipper companies: apply at rahtis.eu/en/apply, and once the company is approved and its details are filled in, keys are created on the API tab of the cabinet. Using the API is free.

Times: API timestamps (created_at, arrival.at, eta.at …) are UTC in ISO 8601. A stop's scheduled_date and scheduled_time are local time in the stop's country (Finland, Sweden, Norway, Denmark) — the same the driver sees.

Money: all amounts are euros excluding VAT (vat_included: false). VAT is added on the invoice according to the country.

Order statuses

An order moves through the statuses below. Every change sends a webhook (order.taken, order.started, order.reopened, order.closed, order.cancelled) and appears in GET /orders/{ref}/events.

deadline_at is the decision deadline: you have 15 minutes from the first offer to choose one, and the chosen carrier has 15 minutes to confirm. When it passes, the order returns to OPEN. A direct assignment (POST /orders/{ref}/assign) has no deadline: the order waits for the vehicle to confirm.

Status
OPENon the offer table, waiting for offers (at most three)
REQUESTEDoffers have arrived — choose one (POST …/offers/{id}/choose) before deadline_at
AWAIT_DRIVERa vehicle is chosen or assigned and waits for the carrier or driver to confirm
IN_PROGRESSconfirmed, the trip is under way: stops are being confirmed
DONEthe trip has ended: fees and the waiting surcharge are fixed, documents are available
CANCELLEDwithdrawn (POST …/withdraw or in the cabinet)
DRAFTa draft in the cabinet, not published — an order created through the API is published at once

Order types and stops

haul_kind says what is moved: TRAILER (semi-trailer) and CONTAINER (container_feet required) move a numbered unit, and trailer_plate (trailer registration or ISO 6346 container number) is required. VAN and TRUCK are express transport inside the vehicle: ldm (loading metres) and cargo_weight_kg on at least one stop are required.

Stops are given in driving order (2–20). Every order has one PICKUP. The table shows what each type requires; a missing stop returns 422 with the reason in details.

Send the contract in the contract field: RAHTIS (default) — the RAHTIS service, service fee 5 % of the price, at least 15 € per job together with the carrier fee; DIRECT — a direct contract with a carrier on the monthly plan, no service fee, and only such carriers see the order. contract is also returned with the order.

Type
TRAILER_SWAP (TRAILER, CONTAINER)Trailer or container swap: PICKUP (collect the unit) → at least one DELIVERY, EXTRA_LOAD or EXTRA_UNLOAD → TRAILER_RETURN (where the unit is left). A transfer run is PICKUP → TRAILER_RETURN with nothing in between: the unit is only moved, and the job is closed with a handover photo instead of a waybill.
ONE_WAY (TRAILER, CONTAINER)The unit is collected and delivered: PICKUP → at least one DELIVERY, EXTRA_LOAD or EXTRA_UNLOAD; TRAILER_RETURN optional.
ROUND_TRIP (TRAILER, CONTAINER)Like ONE_WAY, but the unit comes back — end with TRAILER_RETURN.
ONE_WAY (VAN, TRUCK)Express: PICKUP (loading) → DELIVERY (unloading), with EXTRA_LOAD / EXTRA_UNLOAD in between if needed. order_type is always ONE_WAY.
Role
PICKUPcollection: the unit is picked up or the goods are loaded — always first
DELIVERYunloading at the consignee; company_name required
EXTRA_LOADextra loading on the way; consignee says whom the extra load is for
EXTRA_UNLOADextra unloading on the way
TRAILER_RETURNwhere the empty or loaded unit is left (trailer_loaded says which)

Synchronising orders

GET /orders returns your company's orders by change time, oldest first. Store the updated_at of the last order you processed and pass it next time as updated_since. When the response has next_cursor, fetch the next page with cursor until next_cursor is null.

Filters: status (comma-separated, e.g. OPEN,IN_PROGRESS) and limit (1–100, default 50).

let cursor = null;
do {
  const url = new URL('https://www.rahtis.eu/api/v1/orders');
  url.searchParams.set('updated_since', lastSync);
  if (cursor) url.searchParams.set('cursor', cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RAHTIS_KEY}` } });
  const page = await res.json();
  for (const order of page.data) await save(order);   // lastSync = order.updated_at
  cursor = page.next_cursor;
} while (cursor);

Order, timeline and documents

GET /orders/{ref} returns the order with its stops, progress and — once a carrier has taken it — the vehicle.

GET /orders/{ref}/events is the timeline: status changes, stop arrivals and completions, documents and route amendments.

GET /orders/{ref}/documents returns CMRs and trip photos. Each link is valid for 5 minutes — request the list again when you need the file.

Creating an order

POST /orders publishes the order to the desk under the same rules as the order form. Every stop needs a precise location: send location { lat, lon }, or an address with a house number that the geocoder recognises exactly in the given city. Otherwise the answer is 422 asking for coordinates — we do not guess the neighbouring town.

Distance is calculated as a truck route. The rate is in euros, VAT excluded.

Send an Idempotency-Key header (e.g. your own order number or a UUID). If the connection drops and you retry with the same key, you get the first order back instead of creating a second one. The key is remembered for 24 hours.

curl -X POST https://www.rahtis.eu/api/v1/orders \
  -H "Authorization: Bearer $RAHTIS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: PO-4471" \
  -d '{
    "order_type": "ONE_WAY",
    "shipper_ref": "PO-4471",
    "rate": { "amount": 300 },
    "trailer": "Curtainsider 13.6 m",
    "trailer_plate": "ABC-123",
    "stops": [
      { "role": "PICKUP", "address": "Satamatie 1", "city": "Hanko", "country": "FI",
        "location": { "lat": 59.8208, "lon": 22.9565 },
        "scheduled_date": "2026-10-05", "cargo_weight_kg": 20000 },
      { "role": "DELIVERY", "address": "Tikkurilantie 10", "city": "Vantaa", "country": "FI",
        "company_name": "Vastaanottaja Oy", "scheduled_date": "2026-10-05" }
    ]
  }'
HTTP/1.1 422
{
  "error": {
    "code": "unprocessable",
    "message": "Some stops are invalid.",
    "details": [
      { "field": "stops[1].address",
        "issue": "address could not be located precisely; send location { lat, lon }" }
    ]
  }
}

Withdrawing an order

POST /orders/{ref}/withdraw cancels the order; the body { "reason": "…" } is optional. A completed trip cannot be withdrawn (409).

curl -X POST https://www.rahtis.eu/api/v1/orders/RS-2026-0142/withdraw \
  -H "Authorization: Bearer $RAHTIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer cancelled" }'

Following the trip

Every stop has eta (estimated arrival, recalculated with traffic after each confirmation), arrival (the driver arrived) and completion (the stop was confirmed). Arrival and completion carry the time and a position: the device location at that moment plus distance_m, the distance to the stop address. The distance tells whether the driver was there — for example to settle waiting time.

GET /orders/{ref}/documents returns captured_position (where it was taken and the distance to the stop) for every photo and CMR, and damage for damage photos.

A position is one point from that moment, not continuous tracking. Points are kept for 24 months after the trip ends.

GET https://www.rahtis.eu/api/v1/orders/RS-2026-0142

"stops": [{
  "sequence": 1, "role": "DELIVERY", "city": "Vantaa",
  "eta": { "at": "2026-10-05T12:40:00Z", "source": "TRAFFIC", "updated_at": "2026-10-05T10:02:11Z" },
  "arrival": { "at": "2026-10-05T12:31:08Z",
               "position": { "lat": 60.2936, "lon": 25.0381, "accuracy_m": null, "distance_m": 24 } },
  "completion": { "at": "2026-10-05T13:05:40Z",
                  "position": { "lat": 60.2935, "lon": 25.0379, "accuracy_m": 12, "distance_m": 18 } }
}]

Offers and assigning a vehicle

GET /orders/{ref}/offers shows the vehicles offered by carriers, with driver and rating. POST /orders/{ref}/offers/{id}/choose chooses an offer.

A known vehicle can be assigned directly: GET /vehicles lists the vehicles that have driven for you, and POST /orders/{ref}/assign { "vehicle_id" } assigns one to an order that has no offers yet. The carrier and the driver are notified.

POST /orders/{ref}/unassign cancels the assignment before the trip starts, and the order returns to the desk.

curl https://www.rahtis.eu/api/v1/vehicles -H "Authorization: Bearer $RAHTIS_KEY"

curl -X POST https://www.rahtis.eu/api/v1/orders/RS-2026-0142/assign \
  -H "Authorization: Bearer $RAHTIS_KEY" -H "Content-Type: application/json" \
  -d '{ "vehicle_id": "9451acc9-5893-4741-b2c1-21f1ecbf0113" }'

Changes in progress and rating

During the trip a stop can be changed: PATCH /orders/{ref}/stops/{sequence} — only the fields you send change, null clears a field. POST /orders/{ref}/stops adds an extra loading or unloading (before_sequence says where), and DELETE /orders/{ref}/stops/{sequence} removes a stop not yet passed. Pickup and trailer return cannot be removed. The carrier is notified and the route is recalculated.

POST /orders/{ref}/reprice { "rate": { "amount" } } changes the rate; the distance defaults to the recalculated route. GET /orders/{ref}/amendments lists the changes and whether the carrier has acknowledged them.

After the trip, POST /orders/{ref}/rating { "score": 1–5, "comment" } rates the carrier; a new rating replaces the previous one.

curl -X PATCH https://www.rahtis.eu/api/v1/orders/RS-2026-0142/stops/1 \
  -H "Authorization: Bearer $RAHTIS_KEY" -H "Content-Type: application/json" \
  -d '{ "scheduled_time": "14:30", "note": "Gate 3", "contact": { "name": "Anna", "phone": "+358401234567" } }'

curl -X POST https://www.rahtis.eu/api/v1/orders/RS-2026-0142/reprice \
  -H "Authorization: Bearer $RAHTIS_KEY" -H "Content-Type: application/json" \
  -d '{ "rate": { "amount": 360 } }'

Claims

GET /claims lists the claims filed by your company and against it; GET /claims/{ref} shows messages and attachments (links valid for 5 minutes).

POST /claims { "order_ref", "kind", "description", "stop_sequence"?, "amount"? } files a claim for a trip in progress or completed. POST /claims/{ref}/comments adds a message and POST /claims/{ref}/attachments a file (multipart/form-data: file and note; PDF, JPEG, PNG or WebP up to 10 MB). The other party and the operator are notified by email, as when filing from the cabinet.

curl -X POST https://www.rahtis.eu/api/v1/claims \
  -H "Authorization: Bearer $RAHTIS_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: claim-PO-4471" \
  -d '{ "order_ref": "RS-2026-0142", "kind": "DOWNTIME", "stop_sequence": 1,
        "description": "Waited 2 h at the unloading gate.", "amount": { "amount": 120 } }'

curl -X POST https://www.rahtis.eu/api/v1/claims/CL-RS-2026-0142-1/attachments \
  -H "Authorization: Bearer $RAHTIS_KEY" \
  -F "file=@waiting-slip.pdf;type=application/pdf" -F "note=Gate log"
kind
CARGO_DAMAGEdamage to the cargo or trailer
SHORTAGEshortage
DOWNTIMEwaiting time
DEVIATIONdeviation from the route or schedule
OTHERother

Webhooks

In the cabinet, enter an https URL and choose the events. The signing secret is shown once. The URL must be public (port 443); redirects are not followed.

A notification carries the order number and what changed — fetch GET /orders/{ref} for the full order when you need it.

Answer 2xx within 10 seconds and do the work afterwards. Otherwise the delivery is retried after 1, 5, 15 and 60 minutes, then 3, 6, 12 and 24 hours. The same notification can arrive twice: recognise it by id. Notifications are sent in parallel and may arrive out of order — order them by created_at. The Send test button in the cabinet sends a ping event.

EventWhen
offer.receivedvedaja pakkus autot või auto määrati otse
order.takenvedaja võttis tellimuse
order.startedjuht kinnitas ja vedu algas
order.reopenedtellimus läks tagasi börsile
order.stop_arrivedjuht jõudis marsruudi peatusse
order.stop_completedmarsruudi peatus läbitud
order.eta_changedeeldatav saabumine muutus 5 minutit või rohkem
order.lateauto hilineb peatusse üle 30 min
order.waitingtasuta ootetund peatuses on lõppenud
order.amendedmarsruuti või hinda muudeti
order.closedvedu lõppes
order.cancelledtellimus tühistati
document.addedlisati veoseleht või foto
claim.updatedesitati pretensioon või lisandus sõnum, fail või uus staatus
POST /your/webhook
Rahtis-Event: order.stop_completed
Rahtis-Delivery: 3f1c9a52-6a0e-4c43-9d7e-5b2f0e8d1a44
Rahtis-Signature: t=1790000000,v1=5d1e…

{
  "id": "3f1c9a52-6a0e-4c43-9d7e-5b2f0e8d1a44",
  "type": "order.stop_completed",
  "created_at": "2026-10-05T09:12:44.120Z",
  "data": {
    "ref": "RS-2026-0142",
    "shipper_ref": "PO-4471",
    "stop": { "sequence": 0, "role": "PICKUP", "city": "Hanko",
              "completed_at": "2026-10-05T09:12:43.980Z" }
  }
}

Verifying the signature

Header Rahtis-Signature: t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 with your secret over the string "<t>.<raw body>". Compute it over the raw body before JSON parsing, compare in constant time, and reject when t is older than 5 minutes.

Test vector: secret whsec_test, t=1700000000, body {} → v1 = 35495024f4ef3f94e5a93e22221544c4b75e9a42300cd965ab81cb85cd994e91.

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (Buffer or string), before JSON.parse
function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && parts.v1?.length === 64 &&
    crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
}
import hmac, hashlib, time

def verify(header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(parts.get("v1", ""), expected)

Errors

An error body is { "error": { "code", "message", "details"? } }. details names the field and the issue when the request is at fault.

CodeMeaning
bad_requestthe request is malformed (400)
unauthorizedthe key is missing, wrong or revoked (401)
forbiddenread-only key, inactive company, or the current terms are not accepted (403)
not_foundno such order, stop, offer or claim in your company (404)
conflictthe state does not allow it, or the same Idempotency-Key is being processed (409)
unprocessablethe content is not acceptable: location, route or a business rule (422)
rate_limitedmore than 60 requests per minute (429, Retry-After)
internalunexpected error; the request was not completed (500)

Limits and logs

60 requests per minute per key. For each request the key, method, path, response code and duration are logged; the log is kept for 90 days. The webhook delivery log is kept for 30 days, and a URL is disabled after 100 consecutive failures.

A key stops working when it is revoked or when the rights of the company or of the user who created it end.

Changes to the API

The version is in the path (/api/v1) and in the description (info.version). New fields, events and values may appear — ignore fields and values you do not know.

Aivomaa Oy · 3592993-6 · Soome