API and webhooks
GateRev sends signed webhooks to your ERP or TMS and gives carriers an API to announce visits, assign drivers and vehicles, and report arrival times. This page is built from the machine-readable schemas that are generated from the code which sends and receives these messages.
Field names, codes and descriptions below are copied from the schema files, so they stay in English on every language version.
Webhooks
A site administrator registers an endpoint URL and the event types it receives in GateRev (an empty list means all types). Each signing secret (whsec_…) is shown once — when the endpoint is created and when the secret is rotated. Every delivery is an HTTP POST with a JSON body.
Envelope (schemaVersion 1.0)
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion | const "1.0" | yes | |
id | integer | yes | > 0 |
type | string (102 values, listed below) | yes | |
at | string (date-time) | yes | |
data | any JSON value | yes |
New fields may be added in a MINOR version; ignore fields you do not know. The shape of data depends on the event type.
Headers
| Header | Value |
|---|---|
X-GateRev-Event | Event type, the same as type in the body. |
X-GateRev-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + raw body)> |
X-GateRev-Signature-Previous | Only while a secret is being rotated (24 h): the same signature made with the previous secret. Rotating again within that window replaces the previous secret — only the most recent previous secret is still used. |
Verifying the signature
Compute the HMAC over the raw body exactly as received — do not re-serialise the parsed JSON. Reject signatures older than 300 s.
import { createHmac, timingSafeEqual } from "node:crypto";
// header = X-GateRev-Signature; rawBody = the request body exactly as received (string)
const [t, v1] = header.split(",").map(p => p.slice(p.indexOf("=") + 1));
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = /^[0-9a-f]{64}$/.test(v1 ?? "")
&& timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
&& Math.abs(Date.now()/1000 - Number(t)) <= 300; // replay window
Delivery and retries
- Success is any
2xxresponse within 10 s; anything else, including a network error, is retried. - Retries after 1 min, 5 min, 30 min, 2 h, 12 h (each ±20 %), 6 attempts in total; then the delivery moves to the dead-letter list, where it is kept for 30 days and a site administrator can send it again.
- Redirects (
3xx) are not followed and count as a failure — register the finalhttps://address. The endpoint must be a publichttps://address. - A retry sends the same body and the same
id; useidto drop duplicates.
Event types (102)
adapter.contract_rejectedadapter.event_id_missingadapter.payload_too_largeanpr.exit_heldgate.deniedanpr.exit_matchedanpr.exit_seenanpr.insufficient_readsanpr.low_confidenceanpr.manual_onlyanpr.match_ambiguousanpr.match_out_of_windowanpr.plate_matchedanpr.stale_readanpr.undated_readanpr.unknown_gateanpr.unknown_platecontroller.barrier.positioncontroller.curtain.brokencontroller.curtain.clearcontroller.heartbeatcontroller.presence.clearedcontroller.presence.detectedcontroller.rejectedcontroller.rate_limitedcontroller.scale.readingcontroller.scale.clearedbarrier.contact_stuckbarrier.manual_openbarrier.openedcert.expiringinduction.passedperson_cert.expiringdispatch.no_free_dockyard.task_advancedyard.chain_failedzone.driver_clearzone.person_detectedzone.vap_tracezone.unload_finishedzone.unload_startedevac.drill_endedevac.drill_startedevac.presentnotif.deadwebhook.fanout_startedwebhook.pingwebhook.paused_inactiveevidence.verify_failedperson.approvedperson.badge_revokedperson.cancelledperson.induction_completedperson.off_siteperson.on_siteperson.registeredperson.rejectedscale.errorscale.invalid_readingscale.tare_anomalyscale.unstablescale.weight_recordedsecurity.antipassbacksecurity.device_clock_skewsecurity.device_rate_limitedsecurity.driver_document_refusedsecurity.pin_invalidsecurity.pickup_code_invalidsecurity.qr_replayedslot.bookedslot.reassignedslot.releaseddriver.document_replacedvisit.approval_stepvisit.approvedvisit.arrivedvisit.at_scalevisit.blockedvisit.cancelledvisit.calledvisit.condition_photovisit.documentvisit.departedvisit.dock_arrivedvisit.dock_assignedvisit.dock_finishedvisit.executor_revokedvisit.driver_availablevisit.driver_unavailablevisit.email_verifiedvisit.entered_yardvisit.exit_authorizedvisit.eta_delayedvisit.no_showvisit.overridevisit.rejectedvisit.slot_moved_by_drivervisit.slot_proposal_decidedvisit.slot_proposedvisit.submittedvisit.unblockedvisit.weighed
Carrier API
One key per carrier and organisation, issued by the site administrator and shown once. The key only sees and announces visits at that organisation's sites.
- Paths are relative to the address of your GateRev instance (the one your organisation signs in to), not to this website.
- Unknown fields in a request body are dropped without an error. A misspelt field (for example sealNo instead of sealNumber) is accepted and has no effect — check field names against the tables below.
- Versioning (current 1.0): a MINOR version may add optional request fields, response fields, reasonCodes and endpoints — ignore what you do not know and branch on the HTTP status for an unknown reasonCode. Removing or renaming a field, making an optional field required, or changing a type or a status code requires a new MAJOR version.
Authentication: Authorization: Bearer cr_… (pattern ^Bearer\s+(cr_[0-9a-f]{32})$).
Limits
- 60 requests per 5 min per key, all endpoints (
429 too_many_requests). Counts every request with a valid key, in a sliding window. - 30 announcement attempts per 1 day per key,
POST /api/carrier/visits(429 carrier_quota). Counts attempts whose body passed validation, including ones then rejected (site, plate, blacklist, duplicate), in a sliding 24 h window; an Idempotency-Key replay is not counted. - 60 assignment attempts per 1 day per key,
PATCH /api/carrier/visits/{id}(429 assign_quota). Counts every attempt, including rejected ones (counted before the body is read), in a sliding 24 h window. - Request body at most 64 KiB (
413 payload_too_large).
Counters are kept in the memory of each server process: a restart resets them. Treat the limits as an approximate upper bound, not a guarantee.
Errors
Errors are application/problem+json; branch on reasonCode, not on title.
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | |
status | integer | yes | ≥ -9007199254740991 |
reasonCode | string | yes | |
detail | string | no |
GET /api/carrier/visits
Visits of this carrier at the sites of the key's organisation, including visits it drives as a linked executor. Driver e-mail and phone are not returned.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
limit | integer | no | ≥ 1, ≤ 200; default 50; Maximum number of visits returned, newest first. |
siteId | string | no | length 5–64; Only visits at this site (must belong to the key's organisation). |
Response 200
| Field | Type | Required | Notes |
|---|---|---|---|
carrier | string | yes | Carrier name bound to the key. |
visits | array of object | yes | |
visits[].visitId | string | yes | |
visits[].state | one of: DRAFT | SUBMITTED | REJECTED | APPROVED | AWAITING_ARRIVAL | ARRIVED | IN_YARD | AT_DOCK | AT_SCALE | WEIGHED | READY_TO_EXIT | DEPARTED | CLOSED | CANCELLED | NO_SHOW | BLOCKED | yes | |
visits[].reason | string | yes | |
visits[].role | one of: carrier | executor | yes | carrier = announced by this carrier; executor = driven for another carrier (driver and vehicle only). |
visits[].plate | string or null | yes | null until a vehicle is assigned. |
visits[].trailerPlate | string or null | yes | |
visits[].createdAt | string (date-time) | yes | |
visits[].etaAt | string (date-time) or null | yes | |
visits[].arrivedAt | string (date-time) or null | yes | |
visits[].departedAt | string (date-time) or null | yes | |
visits[].nalog | object or null | yes | Dispatch order (reference, quantity, unit); null for executors. |
visits[].nalog.ref | string | yes | |
visits[].nalog.quantity | number or null | yes | |
visits[].nalog.unit | string or null | yes | |
visits[].site | object | yes | |
visits[].site.id | string | yes | |
visits[].site.name | string | yes | |
visits[].slot | object or null | yes | |
visits[].slot.startAt | string (date-time) | yes | |
visits[].slot.endAt | string (date-time) | yes | |
visits[].slot.dock | string or null | yes | |
visits[].dock | string or null | yes | Dock the vehicle actually goes to (an assigned dock wins over the booked one). |
visits[].driverName | string or null | yes | |
visits[].etaReason | string or null | yes | |
visits[].driverUnavailableAt | string (date-time) or null | yes | |
visits[].driverUnavailableReason | string or null | yes |
Errors
| Status | reasonCode | When |
|---|---|---|
| 401 | carrier_key_required | Missing, malformed, unknown or revoked key, or the carrier is blocked (one answer for all). |
| 429 | too_many_requests | Per-key request limit exceeded. |
| 400 | validation_failed | Query parameters out of range. |
POST /api/carrier/visits
Announce a visit. The carrier is taken from the key, never from the body; driver and vehicle may be assigned later unless the site requires them at announcement.
Header Idempotency-Key (optional, ^[\x21-\x7e]{8,200}$): Same key and same body within 24 h return the first successful (201) response again, with replayed: true; same key with a different body returns 409. A rejected request (4xx, 429) does not hold the key — repeating it runs it again.
Request body (application/json)
| Field | Type | Required | Notes |
|---|---|---|---|
driverName | string | no | length 2–200 |
driverPhone | string | no | length 0–30 |
driverEmail | string (email) | no | Given together with driverName; the driver receives the pass link. |
docType | one of: id_card | passport | driving_licence | no | Optional driver document. The number is stored as a keyed hash plus its last 3 characters in readable form (for comparison at the gate). Given together with docNumber. |
docNumber | string | no | length 0–40 |
pickupCode | string | no | length 4–40; Pickup code issued by the ordering party; requires dispatchRef and plate. |
plate | string | no | length 3–20 |
trailerPlate | string | no | length 0–20 |
containerNo | string | no | length 0–20; ISO 6346 container number (check digit is verified). |
sealNumber | string | no | length 0–60 |
reason | one of: loading | unloading | dropoff_trailer | pickup | other | yes | |
dispatchRef | string | no | length 0–60 |
cargoDesc | string | no | length 0–500 |
quantity | number | no | > 0, ≤ 1000000; Announced quantity; requires dispatchRef. |
unit | one of: kg | no | |
etaAt | string (date-time) | no | Expected arrival. ISO 8601 in UTC: must end in Z — an offset such as +02:00 is rejected. |
locale | one of: en | sl | de | sr | hr | pl | ro | no | default "en"; Language of the driver's pass and messages. |
siteId | string | no | length 5–64; Required when the organisation has more than one site. |
startAt | string (date-time) | no | Requested slot start; the outcome is reported in slotBooked. ISO 8601 in UTC: must end in Z — an offset such as +02:00 is rejected. |
Response 201
| Field | Type | Required | Notes |
|---|---|---|---|
visitId | string | yes | |
statusUrl | string | no | Driver pass link — a credential; hand it to the driver only. |
autoApproved | boolean | yes | |
slotBooked | boolean | no | Present when startAt was sent: whether the slot was booked. |
quantityApplied | const false | no | |
replayed | const true | no | Present when the response is a replay for a repeated Idempotency-Key. |
Errors
| Status | reasonCode | When |
|---|---|---|
| 401 | carrier_key_required | Missing, malformed, unknown or revoked key, or the carrier is blocked (one answer for all). |
| 429 | too_many_requests | Per-key request limit exceeded. |
| 403 | csrf_origin_invalid | An Origin header is present and invalid (requests without Origin/Referer are not affected). |
| 403 | csrf_origin_mismatch | An Origin header is present and foreign. |
| 403 | csrf_referer_invalid | A Referer header is present and invalid. |
| 403 | csrf_referer_mismatch | A Referer header is present and foreign. |
| 413 | payload_too_large | Body larger than the limit. |
| 400 | body_aborted | Body not received completely. |
| 400 | bad_json | Body is not valid JSON. |
| 400 | validation_failed | Body does not match the schema; detail lists the fields. |
| 409 | terms_not_accepted | The carrier's administrator has not accepted the carrier portal terms. |
| 409 | rules_not_accepted | The key was issued under booking rules that are no longer current. |
| 400 | idempotency_key_invalid | Idempotency-Key does not match the pattern. |
| 409 | idempotency_key_reused | Idempotency-Key already used with a different body. |
| 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running — retry. |
| 429 | carrier_quota | Daily announcement limit for this key exceeded. |
| 404 | no_site | siteId is not a site of the key's organisation. |
| 400 | site_required | The organisation has several sites (or none) and siteId was not sent. |
| 400 | assignment_required | The site requires plate and/or driver at announcement. |
| 400 | doc_jmbg_nikad | A national personal number was sent as a document number — never accepted. |
| 400 | doc_kratak | Document number too short. |
| 400 | doc_nepoznat_tip | Unknown document type. |
| 400 | doc_partial | docType and docNumber must be sent together. |
| 400 | driver_partial | driverName and driverEmail must be sent together. |
| 400 | quantity_requires_order | quantity sent without dispatchRef. |
| 400 | invalid_plate | Plate cannot be normalised. |
| 400 | invalid_trailer_plate | Trailer plate cannot be normalised. |
| 400 | invalid_container | Container number is not ISO 6346. |
| 400 | invalid_container_check_digit | Container number check digit is wrong. |
| 400 | invalid_seal | Seal number is not valid. |
| 400 | pickup_code_needs_ref | pickupCode requires dispatchRef. |
| 400 | pickup_code_needs_plate | pickupCode requires plate. |
| 400 | pickup_code_not_expected | The order does not use a pickup code. |
| 400 | pickup_code_invalid | Pickup code does not match the order. |
| 400 | pickup_code_locked | Too many wrong codes for this order — the code is checked at the gate. |
| 403 | blacklisted | Registration rejected (the reason is not disclosed). |
| 409 | duplicate_visit | An open visit already exists for this vehicle and driver that day at this site. |
PATCH /api/carrier/visits/{id}
Assign or replace driver, vehicle or trailer on the carrier's own visit. Replacing the driver rotates the pass link; the old link stops working.
Request body (application/json)
| Field | Type | Required | Notes |
|---|---|---|---|
driverName | string | no | length 2–200 |
driverEmail | string (email) | no | length 0–200 |
driverPhone | string | no | length 0–30 |
plate | string | no | length 3–20 |
trailerPlate | string | no | length 0–20 |
Response 200
| Field | Type | Required | Notes |
|---|---|---|---|
ok | const true | yes | |
driverReplaced | boolean | yes | |
vehicleChanged | boolean | yes | |
linkRotated | boolean | yes | true when the driver changed: the old pass link stops working. |
statusUrl | string | no | New pass link, present only when linkRotated is true. |
Errors
| Status | reasonCode | When |
|---|---|---|
| 401 | carrier_key_required | Missing, malformed, unknown or revoked key, or the carrier is blocked (one answer for all). |
| 429 | too_many_requests | Per-key request limit exceeded. |
| 403 | csrf_origin_invalid | An Origin header is present and invalid (requests without Origin/Referer are not affected). |
| 403 | csrf_origin_mismatch | An Origin header is present and foreign. |
| 403 | csrf_referer_invalid | A Referer header is present and invalid. |
| 403 | csrf_referer_mismatch | A Referer header is present and foreign. |
| 413 | payload_too_large | Body larger than the limit. |
| 400 | body_aborted | Body not received completely. |
| 400 | bad_json | Body is not valid JSON. |
| 400 | validation_failed | Body does not match the schema; detail lists the fields. |
| 429 | assign_quota | Daily assignment limit for this key exceeded. |
| 404 | not_found | No such visit for this carrier and organisation. |
| 409 | assignment_locked | The vehicle has already entered or the visit is closed — the site changes it. |
| 400 | driver_partial | driverName and driverEmail must be sent together. |
| 400 | invalid_plate | Plate cannot be normalised. |
| 400 | invalid_trailer_plate | Trailer plate cannot be normalised. |
| 400 | no_change | Nothing to change. |
| 409 | assignment_cutoff | The site's change deadline has passed — the site changes it. |
| 409 | assignment_needs_review | After manual approval the site changes driver and vehicle. |
| 409 | pickup_code_bound | The vehicle is bound to the order's pickup code. |
| 409 | pickup_code_driver_bound | The driver is bound to the order's pickup code. |
| 403 | blacklisted | Assignment rejected. |
| 409 | watchlist_review | This change must be approved by the site. |
| 409 | duplicate_visit | An open visit already exists for this vehicle and driver that day. |
| 409 | conflict | The visit changed state meanwhile — read it again. |
PUT /api/carrier/visits/{id}/eta
Report an absolute ETA from the carrier's TMS or telematics, optionally with a delay reason.
Request body (application/json)
| Field | Type | Required | Notes |
|---|---|---|---|
etaAt | string (date-time) | yes | Absolute arrival time. ISO 8601; a UTC offset such as +02:00 is allowed. |
reason | one of: saobracaj | utovar | pauza | kvar | granica | no | Delay reason: saobracaj = traffic, utovar = delay at loading, pauza = mandatory rest break (Regulation (EC) 561/2006), kvar = breakdown, granica = border crossing. |
Response 200
| Field | Type | Required | Notes |
|---|---|---|---|
ok | const true | yes | |
etaAt | string (date-time) | yes |
Errors
| Status | reasonCode | When |
|---|---|---|
| 401 | carrier_key_required | Missing, malformed, unknown or revoked key, or the carrier is blocked (one answer for all). |
| 429 | too_many_requests | Per-key request limit exceeded. |
| 403 | csrf_origin_invalid | An Origin header is present and invalid (requests without Origin/Referer are not affected). |
| 403 | csrf_origin_mismatch | An Origin header is present and foreign. |
| 403 | csrf_referer_invalid | A Referer header is present and invalid. |
| 403 | csrf_referer_mismatch | A Referer header is present and foreign. |
| 413 | payload_too_large | Body larger than the limit. |
| 400 | body_aborted | Body not received completely. |
| 400 | bad_json | Body is not valid JSON. |
| 400 | validation_failed | Body does not match the schema; detail lists the fields. |
| 404 | not_found | No such visit for this carrier and organisation. |
| 409 | not_expected | The visit is not waiting for arrival. |
| 400 | eta_in_past | ETA is in the past. |
| 409 | eta_too_late | ETA is beyond the allowed ceiling after the slot start (or after the first reported ETA). |
Schema files
Both files are generated from the code. webhook-envelope.schema.json (urn:gaterev:webhook:envelope:1) is a JSON Schema draft 2020-12 document for the webhook body. carrier-api.schema.json (urn:gaterev:carrier-api:1) is not itself a schema: it is a collection that lists, per endpoint, the method, path, errors and a JSON Schema draft 2020-12 for the query, request body and response. Ask us for the current files.
Last updated: 2026-10-13