API i webhookovi
GateRev šalje potpisane webhook poruke vašem ERP-u ili TMS-u, a prijevoznicima daje API za najavu posjeta, dodjelu vozača i vozila te javljanje vremena dolaska. Ova se stranica gradi iz strojno čitljivih shema koje nastaju iz koda koji te poruke šalje i prima.
Tehnički sadržaj u nastavku je na engleskom, kao i same sheme: nazivi polja, kodovi i opisi preuzimaju se iz njih doslovno.
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.
Posljednja izmjena: 2026-10-13