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.
Zending en versies
Section titled “Zending en versies”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.
Levenscyclus
Section titled “Levenscyclus” issue pickup signature delivery signaturedraft ───────────▶ 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.
Handtekeningen en trust-levels
Section titled “Handtekeningen en trust-levels”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.
Documenten
Section titled “Documenten”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.
Integriteit: de hash-keten
Section titled “Integriteit: de hash-keten”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.
Zelf narekenen
Section titled “Zelf narekenen”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.
Ritten en stops
Section titled “Ritten en stops”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.
Providers en routing
Section titled “Providers en routing”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.
Test en live
Section titled “Test en live”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.