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
62 outbound events. Legacy names are shown where an integration without a subscription still receives them.
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).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[]).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).
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.
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).
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. |
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. |
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. |
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). |
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.json |
| 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