Ga naar inhoud

Concepten

Eén resource staat in het midden: de zending. Alles wat Cargofollow doet, hangt eraan vast — partijen, goederen, documenten, handtekeningen en gebeurtenissen. Deze pagina legt het model uit waar de quickstart en de API-reference op staan.

Een zending krijgt bij POST /v1/shipments een shp_-id dat nooit verandert. Daarnaast heeft ze een versie: een momentopname van alle vrachtbriefvelden, met een eigen ver_-id en een eigen document_hash.

Elke inhoudelijke wijziging maakt een nieuwe versie. De oude blijft bestaan en blijft opvraagbaar, want een handtekening hoort bij de versie die iemand ondertekende — niet bij “de zending” in het algemeen. Zo kun je een jaar later nog aantonen wát er precies getekend is.

Welke velden nog mogen veranderen, hangt af van de status:

Fase Wat vastligt Wat nog mag veranderen
draft Niets Alles
issued, in_transit Partijen, adressen, goederen, kosten, transport_type, provider Chauffeur, voertuig, tijdvensters, contactpersonen, instructies, metadata
delivered, completed, cancelled Alles Niets

Een poging om een bevroren veld te wijzigen geeft invalid_state; de volledige lijst staat in @freightapi/core/state.

issue pickup signature delivery signature
draft ───────────▶ issued ─────────────────────▶ in_transit ─────────────────▶ delivered
│ │ │ │
│ cancel │ cancel │ cancel │ (automatisch)
▼ ▼ ▼ ▼
cancelled cancelled cancelled completed
Van Naar Waardoor
draft issued POST /v1/shipments/{id}/issue
issued in_transit Handtekening van de vervoerder bij het laden
in_transit delivered Handtekening van de geadresseerde bij het lossen
delivered completed Wanneer alle vereiste handtekeningen binnen zijn
draft, issued, in_transit cancelled POST /v1/shipments/{id}/cancel

De state machine staat in @freightapi/core/state en is de enige plek waar overgangen zijn gedefinieerd. Wat er niet in staat, kan niet: een verboden overgang geeft invalid_transition, ook als je hem via een omweg probeert. Afgeleverd is eindstation — cancelled na delivered bestaat niet.

Elke overgang schrijft een gebeurtenis en dus een schakel in de hash-keten.

Een handtekening in Cargofollow is vier dingen tegelijk: een rol, een soort, een methode en een vertrouwensniveau.

Dimensie Waarden Betekenis
Rol consignor, carrier, consignee CMR-vakken 22, 23 en 24: afzender, vervoerder, geadresseerde
Soort signature, seal Een natuurlijk persoon tekent, een rechtspersoon zegelt
Methode drawn, click_otp, api, photo_of_paper Hoe de handtekening tot stand kwam
Vertrouwensniveau platform_auth, ades, qes Hoe zwaar hij weegt in een geschil

De drie vertrouwensniveaus, van licht naar zwaar:

  • platform_auth — Cargofollow stelt vast wie tekende via de link, het token en eventueel een eenmalige code per e-mail of sms. Dit is marktpraktijk in e-CMR en de standaard.
  • ades — een geavanceerde elektronische handtekening onder eIDAS: uniek aan de ondertekenaar gekoppeld en aan de data, zodat elke wijziging achteraf zichtbaar is.
  • qes — een gekwalificeerde handtekening, afgegeven met een gekwalificeerd certificaat van een QTSP. In de hele EU gelijkgesteld aan een natte handtekening.

Elke handtekening draagt bewijsmateriaal: het tijdstip, het document_hash van de versie die is getekend, de methode, en afhankelijk daarvan een handtekeningafbeelding, een OTP-bevestiging of een certificaatketen. Dat bewijs is wat een handtekening bruikbaar maakt in een discussie — de afbeelding zelf is het minst interessante deel.

Elk bestand dat aan een zending hangt, is een document met een doc_-id, een soort, een mimetype en een sha256 van de bytes zoals ze zijn opgeslagen.

Soort Wat het is
ecmr_pdf De vrachtbrief als PDF, per versie opnieuw gerenderd
pod_pdf Het afleverbewijs, met de handtekeningen erop
attachment Wat je zelf uploadt: een pakbon, een douanedocument
signature_image De getekende krabbel bij een drawn-handtekening
photo Een foto bij een voorbehoud

Downloaden gaat via een kortlevende gesigneerde URL, nooit via een permanente link. Een verlopen link geeft 410; vraag er dan gewoon een nieuwe op. De sha256 in het document-object is wat je vastlegt als je wilt kunnen bewijzen dat je later hetzelfde bestand terugkreeg.

Elke zending heeft een append-only keten van gebeurtenissen. Elke schakel draagt de hash van zijn voorganger, dus je kunt achteraf niets tussenvoegen, weghalen of herschrijven zonder dat alles erna niet meer klopt.

Per schakel:

hash = sha256_hex(prev_hash + canonical_json({ id, shipment_id, seq, type, occurred_at, actor, payload }))

Met prev_hash = "" voor seq = 1, en daarna telkens de hash van de vorige schakel. Precies deze zeven velden tellen mee — prev_hash, hash en alles wat de API er later omheen zet niet. canonical_json is JSON zonder witruimte, met objectsleutels recursief gesorteerd op UTF-16-code-units (in de geest van RFC 8785), gehasht als UTF-8.

GET /v1/shipments/{id}/integrity rekent de keten server-side na:

{
"ok": true,
"events": 7,
"head_hash": "9f2c…",
"seq_gaps": [],
"verified_at": "2026-09-14T10:00:00.000Z"
}

Ook bij ok: false is het antwoord een 200 — een gebroken keten is een bevinding, geen fout in je verzoek. broken_at zegt dan bij welke seq het misging.

Vertrouw niet alleen op ons eigen oordeel: haal de schakels op met GET /v1/shipments/{id}/events en reken de keten na met je eigen code.

import { createHash } from 'node:crypto'
/** JSON zonder witruimte, sleutels recursief gesorteerd. */
function canonical(value) {
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
if (value !== null && typeof value === 'object') {
const entries = Object.keys(value)
.sort()
.filter((key) => value[key] !== undefined)
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
return `{${entries.join(',')}}`
}
return JSON.stringify(value)
}
export function verifyChain(events) {
let prev = ''
for (const event of events) {
const link = canonical({
id: event.id,
shipment_id: event.shipment_id,
seq: event.seq,
type: event.type,
occurred_at: event.occurred_at,
actor: event.actor,
payload: event.payload,
})
const hash = createHash('sha256').update(prev + link, 'utf8').digest('hex')
if (hash !== event.hash) return { ok: false, broken_at: event.seq }
prev = hash
}
return { ok: true, head_hash: prev }
}

Bewaar de head_hash op het moment dat je hem leest. Daarmee kun je later aantonen dat er niets aan de zending is toegevoegd of veranderd na dat moment, zonder dat je ons daarvoor hoeft te geloven.

Een chauffeur rijdt zelden één zending. POST /v1/tours groepeert er meerdere tot één rit, in de volgorde waarin ze gereden wordt.

De eenheid van een rit is een stop, geen zending. Eén zending geeft een chauffeur tot twee stops die uren en kilometers uit elkaar liggen: het laden, waar de vervoerder voor overname tekent, en het lossen, waar de geadresseerde voor ontvangst tekent. Een lijst van zendingen kan niet zeggen dat A en B eerst allebei worden opgehaald voordat er één wordt afgeleverd — en precies dat is multi-drop.

{
"reference": "Maandag noord",
"driver": { "name": "Marek Nowak", "phone": "+31612345678" },
"stops": [
{ "shipment_id": "shp_…A", "type": "pickup" },
{ "shipment_id": "shp_…B", "type": "pickup" },
{ "shipment_id": "shp_…A", "type": "delivery" }
]
}

Vier dingen zijn het waard om te weten.

De volgorde is van jou. Cargofollow plant en optimaliseert niets; position is 1-gebaseerd en volgt de array die je stuurt. Herordenen doe je met een PATCH die de hele lijst vervangt — een positie betekent niets zonder de andere posities.

Een zending zit in hoogstens één actieve rit. Een tweede rit die hem opeist krijgt conflict met het ritnummer dat hem al heeft. Zet je de rit op completed of cancelled, dan laat hij zijn zendingen los en kunnen ze opnieuw ingepland worden.

done wordt afgeleid, niet opgeslagen. Een stop is klaar zodra de handtekening bestaat die hij vraagt: carrier bij laden, consignee bij lossen. Daarom kan de voortgang van een rit nooit uiteenlopen met de vrachtbrieven die hij telt.

Er komt geen nieuw eventtype bij. Een zending die een rit in of uit gaat, meldt zich als shipment.updated met changed_paths: ["tour_id"]. Volg je een zending al, dan hoor je het vanzelf.

De chauffeur ziet dezelfde rit terug in het chauffeursoverzicht: stops op volgorde, een navigatielink per adres, en per stop de knop die de tekenflow opent.

Een zending wordt uitgevoerd door een provider: de partij die de eCMR juridisch uitgeeft. Cargofollow kan dat zelf (native), maar kan ook naar een externe erkende leverancier routeren.

Provider Wat het is
native Cargofollow geeft zelf uit
mock Alleen in de sandbox: doet alsof, zonder externe aanroep
pionira, transfollow, dashdoc, olf Externe eCMR-platformen

Je hoeft niets te kiezen. Laat je provider weg, dan kiest de routing er een op grond van de corridor — en dat is precies waar het om gaat bij België, waar de Benelux-pilot alleen e-CMR toestaat via een door de NIWO erkende leverancier. Zie Landvereisten.

Kies je er wel zelf een, dan wordt die gerespecteerd, ook als de routing iets anders zou hebben gedaan. Je krijgt de regel dan terug als bevinding in warnings, niet als blokkade. Synchroniseren met een externe provider kan mislukken; dat meldt zich als provider.sync_failed met een will_retry-vlag.

De prefix van je key bepaalt de modus, en niets anders:

Sandbox Live
Key sk_test_ sk_live_
Basis-URL https://api.eftisandbox.app https://api.cargofollow.com
Externe providers Nooit aangeroepen Echt aangeroepen
Simuleren Toegestaan Nooit
Facturatie Nee Ja

De twee datasets raken elkaar nooit. Een sk_test_-key ziet geen live-zendingen en omgekeerd; ook een id uit de ene modus bestaat niet in de andere. Een live-route aanroepen met een organisatie die daar nog niet voor is vrijgegeven, geeft live_not_enabled.

Validatie, state machine en hash-keten zijn in beide modi identiek. Wat je in de sandbox werkend krijgt, werkt live hetzelfde.