Build on YARDtwin
A documented REST API, scoped keys and signed webhooks for connecting yard and dock operations to your ERP, WMS, TMS, weighbridges and cameras.
Overview
Quick start
- In the app, open Site Admin → Access & security → API keys and create a key. Pick only the scopes you need (for example appointments:read). Write scopes are marked and must be granted explicitly.
- Copy the key when it is shown — it starts with yt_ and is displayed once. Store it as a secret.
- Send it as a bearer token on every request:
Authorization: Bearer $YARDTWIN_API_KEYA first call — list the bookings for a day (scope appointments:read). YARDTWIN_API_KEY is the key you copied in step 2, kept in an environment variable rather than typed into the command:
curl -s "https://yardtwin.com/api/v1/appointments?date=2026-10-01&limit=10" \
-H "Authorization: Bearer $YARDTWIN_API_KEY"| Topic | Detail |
|---|---|
| Base URL | https://yardtwin.com/api/v1 |
| Scopes | Deny-by-default. An operation not mapped to one of the key’s scopes returns 403. The scope for each operation is listed in the API reference (x-yardtwin-scope). |
| Rate limit | 500 requests per minute per client IP address across the API. Standard RateLimit headers are returned; above the limit you receive HTTP 429 — back off and retry. |
| Errors | JSON bodies of the form { "error": { "message": "…", "code": "…" } } with conventional HTTP status codes. |
| Carrier / TMS API | Carriers integrate with their own yk_ keys issued per carrier; see the API reference. |
Webhooks
Add an integration in Site Admin → Integrations with your HTTPS endpoint, and choose the events it subscribes to. With no subscription an integration receives every event. Each delivery is a JSON POST:
{
"event": "gate.checked_in",
"source": "YARDtwin",
"delivery_id": "6f1c0b0e-2d0a-4c1e-9a55-0c8f4f7e2a11",
"timestamp": "2026-10-01T07:42:10.512Z",
"site_id": "…",
"data": { "…": "event-specific fields" }
}| Header | Meaning |
|---|---|
X-YARDtwin-Signature | sha256=<hex> — HMAC-SHA256 of the raw body + ":" + timestamp, keyed with the integration’s signing secret. |
X-YARDtwin-Timestamp | Unix time in seconds. Re-signed with a fresh timestamp on every attempt. |
X-YARDtwin-Event | The event name, e.g. gate.checked_in. |
X-YARDtwin-Delivery | Stable delivery id — identical across retries and replays. Also sent as Idempotency-Key and in the body as delivery_id. |
X-YARDtwin-Attempt | Attempt number, starting at 1. |
X-YARDtwin-Replay | “true” when the delivery is a replay of a dead-lettered message. |
X-YARDtwin-Sandbox | “true” when the event comes from a sandbox tenant (the body also carries "sandbox": true). |
Verify the signature
Compute the HMAC over the exact bytes you received — do not re-serialise the JSON first — and compare in constant time.
const crypto = require('crypto');
// Express: capture the raw body, e.g. app.use(express.raw({ type: 'application/json' }))
function verifyYardtwin(req, secret) {
const signature = req.get('X-YARDtwin-Signature'); // "sha256=<hex>"
const timestamp = req.get('X-YARDtwin-Timestamp'); // unix seconds
const body = req.body.toString('utf8'); // the exact bytes received
if (!signature || !timestamp) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute window
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body + ':' + timestamp)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib, hmac, time
def verify_yardtwin(raw_body: bytes, headers: dict, secret: str) -> bool:
signature = headers.get("X-YARDtwin-Signature", "") # "sha256=<hex>"
timestamp = headers.get("X-YARDtwin-Timestamp", "") # unix seconds
if not signature or not timestamp:
return False
if abs(time.time() - int(timestamp)) > 300: # 5-minute window
return False
message = raw_body + b":" + timestamp.encode()
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)Retries, dead-letter and replay
- Respond with any 2xx within 10 seconds to acknowledge. Anything else counts as a failure.
- Failed deliveries are retried up to 5 attempts with exponential backoff and jitter (starting at about 1 second).
- After the last attempt the delivery is dead-lettered and visible in the integration’s delivery log. Dead letters are replayed automatically about 15 minutes later, and an administrator can replay them on demand.
- Delivery is at-least-once: de-duplicate on delivery_id.
Event reference
84 outbound events. Legacy names are shown where an integration without a subscription still receives them.
The live list, with a sample envelope for every event, is always at /api/v1/public/event-catalogue.
Appointments
appointment.createdA booking was created — by ops, the carrier portal, or an inbound integration.appointment.rescheduledA booking’s date, start or end changed (data.previous carries the old values).appointment.reslot_proposedETA re-slotting proposed (or, in apply mode, applied) a new slot for a booking running late (data.kind late|swap, data.from, data.to, data.paired_appointment_id, data.slip_min, data.mode).appointment.cancelledA booking was cancelled by ops (data.reason, data.late_cancellation).appointment.reminderA scheduled reminder for an upcoming booking was issued (data.offset_min).appointment.no_showA booking was marked no-show — automatically after the grace period, or by an operator.appointment.late_arrivalA truck checked in later than the site’s late-arrival tolerance (data.late_by_min).appointment.dock_assignedA dock was assigned or changed — by a planner, a reschedule, or the agent.appointment.status_changedlegacy: appointment:statusGeneric status transition through the status endpoint (data.status, data.previous_status).appointment.customs_changedA booking’s customs status changed — released or held by the customs broker / SAP GTS (customs_release) or by site staff (data.status released|held|pending, data.previous_status, data.mrn, data.procedure, data.direction import|export|transit, data.source the integration name or user, data.released_by).order.unverifiedA booking carries an order reference the ERP / WMS lookup did not recognise (or a duplicate of an active booking); it stays scheduled until a planner confirms it (data.orders[]).order.qty_exceededA booking’s expected quantity for an order is above the ERP’s open quantity plus the site’s tolerance (settings.orders.qtyPolicy warn; under block the booking is refused instead). data.policy, data.orders[] {type, ref, booked_qty, open_qty, allowed_qty, unit, tolerance_pct}.waitlist.joinedA requester joined the waitlist for a full day or window (data.waitlist_id, preferred_date, load_type, material_type, pallet_count, refusal_code, requested_by_type).waitlist.offeredA freed slot was offered to a waitlist request with an expiry (data.offered_date, offered_start, offered_end, offered_dock_id, expires_at, source cancel|no_show|reschedule|sweep|ops).waitlist.fulfilledA waitlist offer was accepted and booked (data.appointment_id, confirmation_code, status; the appointment block is the new booking).waitlist.expiredA waitlist request expired — its dates passed or its offers were exhausted (data.reason).appointment.approval_requestedA carrier / supplier / public booking landed at pending_approval under the site’s approval policy (data.reason new_carrier|restricted_tier|unverified|all_carriers|public_link, data.channel).appointment.approvedA planner approved a pending booking (data.previous_status, data.notes, data.previous; the appointment block carries the confirmed slot).appointment.rejectedA planner rejected a pending booking — it is cancelled with a reason (data.reason_code, data.notes).dg.segregation_conflictA dangerous-goods declaration conflicts with ADR 7.5.2.1 mixed loading — within the load itself, or with an overlapping booking on the same dock (or dock group, where the site checks adjacency). data.level warn|block, data.conflicts[].
Gate
gate.checked_inlegacy: gate:check_inA vehicle was checked in at the gate (v1 check-in or Gate v2 authorise).gate.checked_outlegacy: gate:check_outA vehicle was checked out at the gate.gate.self_checkinA driver submitted a self check-in from their phone; gate staff must approve it.gate.exceptionA drive-through read the system could not settle: no booking (unmatched), two bookings (ambiguous), a dock-rule hold, or a refusal by carrier vetting / qualification (denied). data.exception_id, kind, decision, plate, container_number, camera_id, candidate_ids.gate.refusedEntry was refused at Gate v2 (data.reason_code).gate.evidence_capturedPhotographic evidence was attached to a gate movement — tractor, trailer, seal, plate or placard, on the way in or out (data.direction, count, kinds, gate_transaction_id).gate.walk_up_registeredAn unbooked arrival was registered at the gate as a walk-up booking and put on the yard record (data.purpose, source, truck_reg, checked_in). It waits in the planner's unplanned list until accepted or rejected.gate.walk_up_decidedA planner accepted or rejected an unplanned arrival (data.decision, reason, dock_id). Rejecting does not change the visit status — the truck leaves through the gate.gate.geofence_arrivedA tracked truck entered the site geofence and the booking was set to arrived automatically (data.distance_m, source, checked_in). When the site also checks in, gate.checked_in follows with source geofence.gate.geofence_departedA finished truck stayed outside the geofence for the confirmation window and was checked out automatically (data.minutes_outside, distance_m). gate.checked_out carries the departure itself with source geofence.dg.adr_lapsedA dangerous-goods booking presented at the gate with a missing or expired driver ADR certificate (data.status, code, expires_on, level pass|warn|blocked|overridden).
Dock & warehouse
unload.startedWarehouse work started at the dock — unloading (inbound) or loading (outbound); data.load_type says which.unload.completedWarehouse work completed at the dock (data.pallet_count).incident.raisedA hold was raised or an inspection failed (data.kind, data.reason_code).osd.reportedAn over / short / damage (OS&D) record was made at the dock (data.osd_id, type over|short|damage, quantity, unit, order_id, order_ref, sscc, photo_count, note).dock.sensor_changedA dock sensor changed state (data.dock_id, dock_name, sensor door|leveller|restraint|occupancy|light, state, previous_state, occurred_at, source_integration).dock.safety_warningA dock safety condition was detected from sensor data — e.g. the door opened while the vehicle restraint was not engaged (data.kind, dock_id, dock_name, restraint_state). Advisory: nothing is blocked.
Seals & weights
seal.appliedAn outbound seal was applied and recorded.seal.verifiedAn arrival seal was verified against the expected number and matched (or no expectation existed).seal.mismatchA seal did not match, was tampered or damaged; the booking is on compliance hold.weight.recordedA weighbridge reading was recorded (data.kind gate_in|gate_out|intermediate, weight_kg, source, and — where the reading came from a registered indicator — instrument_id, weighbridge_id, alibi_no, stable and legal_for_trade).weight.ticket_issuedA weighbridge ticket was issued for a visit (data.doc_number, content_hash, instrument ids, alibi numbers, gross/tare/net and whether the weighing was legal for trade).
Departure
departure.signedThe departure clearance was signed (21 CFR Part 11 record; data.signature_hash).departure.refusedThe driver refused to sign the departure clearance (data.reason_code).departure.readyThe load is released to depart — warehouse hand-off, WMS load_ready or the status API (data.source, data.dock_name).
Yard tasks
task.createdA yard task was created — by a rule, the system check-in rule, or a planner.task.completedA yard task was completed by a jockey or planner.task.exceptionA jockey raised a move as an exception (data.code blocked | trailer_missing | damage | wrong_trailer | equipment | other, data.reason).
Trailers
trailer.droppedA trailer was dropped on the yard (drop-and-hook visit or a resident trailer registered on the twin).trailer.hookedA trailer left the yard hooked to a tractor.trailer.movedA trailer was moved to another position (jockey move, manual move).yard.check_closedA yard check (lot audit) was closed — data.expected, found, wrong_spot, unexpected, missing, corrected.yard_check.overdueA scheduled yard check was not started within its grace window (data.schedule_id, schedule_name, due_at, grace_min, assignee_role, zone_scope).
Detention
detention.warningFree time is about to expire (data.remaining_min).detention.startedFree time was used up; detention is accruing (data.free_time_min, data.rate_per_hour).demurrage.startedThe visit crossed the demurrage threshold and became storage.
Documents (CMR / POD)
cmr.issuedA consignment note (CMR / BOL) was issued or re-issued for an outbound load (data.doc_number, version, content_hash, dataset_hash, dataset = the eFTI structured record, pdf_url, verify_url).cmr.signedA party signed the consignment note — consignor (21 CFR Part 11) or carrier acknowledgement (data.party, data.kind, data.signature_hash, new version).cmr.deliveredThe consignee acknowledged receipt through the proof-of-delivery link (data.signature_hash, data.dataset.reservations).
Life sciences
ls.excursion.detectedCold-chain excursion detected.ls.consignment.quarantinedConsignment quarantined after an excursion.ls.excursion.dispositionedExcursion dispositioned by QA.ls.custody.brokenChain of custody broken.ls.consignment.rejectedConsignment rejected.ls.reconciliation.discrepancyReturns reconciliation discrepancy.
Visitors & presence
visitor.arrivedA visitor, contractor or on-foot driver was registered onto the site (data.visitor_id, kind, name, company, host, via staff|kiosk).visitor.departedA visitor signed out and left the on-site presence list (data.visitor_id, kind, name).
Fleet telematics
telematics.gate_inA telematics-tracked tractor or trailer entered the site fence (data.tag_id, subject, distance_m, plate; appointment when the device resolved to a booking).telematics.gate_outA telematics-tracked tractor or trailer left the site fence (data.tag_id, subject, distance_m).telematics.gate_readThe gate read a tracked tractor's plate from its telematics fix at the fence, like an ANPR read (data.decision allow|exception|deny, code, matched_by, exception_id).telematics.at_dockA tracked subject has stood inside a dock location for the dwell window (data.dock_location_id, dock_id, dock_name, dwell_s).telematics.left_dockA tracked subject left the dock location it stood in (data.dock_location_id, dock_id).telematics.stoppedA tracked vehicle has been stationary for the stop window (data.stopped_s).telematics.movingA tracked vehicle started moving again (data.speed_kmh).telematics.ignition_onThe tractor's ignition came on (from the diagnostics feed).telematics.ignition_offThe tractor's ignition went off (from the diagnostics feed).telematics.trailer_droppedThe tractor left the fence while its paired trailer stayed (data.trailer_tag_id, trailer_number, trailer_asset_id).telematics.trailer_collectedA resident trailer's tracker left the fence (data.trailer_tag_id, trailer_number, trailer_asset_id).telematics.pairedA tractor and a trailer were paired (data.tractor_tag_id, trailer_tag_id, source attachment|gate|manual|co_movement).telematics.unpairedA tractor–trailer pairing ended (data.tractor_tag_id, trailer_tag_id, reason).telematics.offlineA device stopped communicating (data.tag_id, last_fix_at).
Stage performance
stage.warningA stage of a visit reached the warning share of its target — truck or trailer (data.subject, stage, next_stage, elapsed_min, target_min, target_basis configured|rule|default|booking|learned|learned_floor, learned_n, learned_level, pct_of_target, since, dock_id, trailer_number, kind live|drop|hook).stage.overdueA stage of a visit passed its target (same data as stage.warning; level overdue). Fires once per visit, subject and stage.
System
connector.testSigned connectivity ping sent by the conformance test.
Inbound events
Systems send events to YARDtwin at POST /api/v1/integrations/{integrationId}/webhook with a JSON body { "event_type": "…", "data": { … } }. Sign it with the integration’s webhook secret: X-Webhook-Signature = sha256=HMAC-SHA256(raw body + ":" + timestamp) and X-Webhook-Timestamp = unix seconds; requests older than 5 minutes are rejected.
TS=$(date +%s)
BODY='{"event_type":"shipment_eta_update","data":{"reference":"PO-45001234","new_date":"2026-10-01","new_time":"09:30"}}'
SIG="sha256=$(printf '%s:%s' "$BODY" "$TS" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')"
curl -s -X POST "https://yardtwin.com/api/v1/integrations/$INTEGRATION_ID/webhook" \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: $TS" \
-H "X-Webhook-Signature: $SIG" \
-d "$BODY"| event_type | What it does |
|---|---|
shipment_created / order_created | Create a booking from a WMS, ERP or TMS shipment or order. |
shipment_eta_update | Move a booking’s date or start time, matched by its reference number. |
shipment_status | Update the status of a booking from the sending system. |
shipment_cancelled | Cancel a booking. |
sap_inbound_delivery | SAP EWM inbound (or outbound) delivery → booking; raw DELVRY07 IDoc envelopes are flattened automatically. Direction: load_type, direction (I / O / B) or the delivery type (LFART); the reply says which decided it. |
sap_outbound_delivery | The same message for an outbound delivery (ODO): the booking is outbound unless load_type says otherwise. |
gr_status / gr_posted / gr_rejected | Goods-receipt acknowledgement from SAP (material document number or rejection). |
gi_status / gi_posted / gi_rejected / goods_issue | Goods-issue acknowledgement for outbound loads. |
gr_reversed / gi_reversed | A goods receipt or goods issue was reversed in SAP; the reversal document is recorded. |
tu_registered | SAP transport-unit acknowledgement for a registered truck. |
door_status | A door was blocked or released in the warehouse system. |
door_assignment | The WMS assigns (or changes) the dock door for a booking. |
load_ready | The WMS reports the load is ready to depart. |
unload_started / loading_started | The WMS reports that work on the load began — the dock screen’s Start, driven by the warehouse system (occurred_at within the last 24 h; a load already started answers action: already). |
unload_completed / loading_completed | The WMS reports that work on the load ended — the dock screen’s Complete (pallet_count optional). No goods movement is posted back: the sender already knows. |
staging_complete | Goods are staged for loading. |
customs_release | Customs clearance status for a held load. |
master_data | Import carriers, materials or other master data. |
plate_read / vision_read | A camera (ANPR / computer vision) read a plate or container number. |
vehicle_detected / occupancy / inspection_event | Camera or sensor events for the yard. |
position_report | Vehicle GPS position from a fleet or telematics system. |
tag_report | Position from a trailer or asset tag (any GPS or RTLS provider, e.g. Wheere). One fix per call, or a batch as { "fixes": [...] }. A fix is dropped as "skipped" when it is less accurate than the integration’s threshold, older than the last accepted fix for that tag, or more than ten minutes in the future. A Wheere integration also accepts Wheere’s own webhook deliveries as they are: verified with the X-Wheere-Signature header, de-duplicated on X-Wheere-Event-Id, and BATTERY events acknowledged and logged. A Geotab integration is normally fed by YARDtwin reading the MyGeotab data feeds, but it also accepts Geotab-shaped bodies here — a LogRecord, a battery StatusData, or a feed page { "typeName": "LogRecord", "data": [...] } — mapped to the same tag_report (tools/geotab-replay posts a recorded day this way). |
temperature_reading | Cold-chain temperature reading from a data logger. |
weight_reading / weighbridge_weight | Gross or tare reading from a weighbridge. |
ecmr_reference | Link an external e-CMR reference to a consignment note. |
edi_asn | Advance ship notice as raw EDIFACT DESADV or X12 856 text in the "edi" field; creates or updates the booking, order lines and expected SSCCs. |
dock_sensor | Dock equipment state: door, leveller, vehicle restraint, occupancy or dock light (warns when a door opens without the restraint engaged). |
pallet_scan | The WMS / SAP EWM echoes a scanned pallet or handling unit, or a temperature it took, against a booking (replaces the retired POST /warehouse/wms/pallet-scan). |
The reply is 200 with { "received": true, "logId": "…" } and, where a field was interpreted rather than read directly, a warnings[] list. Every message appears in the integration’s log in the app.
Resources
| Resource | Where |
|---|---|
| OpenAPI 3.1 specification | /api/v1/openapi |
| Interactive API reference | /api/v1/docs |
| Postman collection | /api/v1/postman |
| Sandbox tenant | Available on Professional and Enterprise — request it via admin@yardtwin.com |
| SAP EWM integration kit | Configuration manual, API specification, request collection and CPI mapping reference — on request via admin@yardtwin.com |
| Integrations overview | Integrations |
Honest boundaries
- EDI (EDIFACT DESADV, X12 856) is accepted over HTTPS webhooks. There is no AS2 or VAN connection yet.
- SAP IDoc mapping runs on the SAP side (for example in SAP Integration Suite / CPI); YARDtwin receives and sends JSON over HTTPS.
- There are no client SDKs yet — generate a client from the OpenAPI specification if you need one.
Building an integration for several customers? Join the partner programme