Quickstart
This quickstart runs entirely in the sandbox on https://api.eftisandbox.app. Nothing is
transported and nothing is billed; sandbox data is separate from live data.
-
Get an API key
Section titled “Get an API key”Every call carries
Authorization: Bearer <key>. The prefix decides the mode:sk_test_talks to the sandbox,sk_live_to production.Put the key in your environment so it stays out of your shell history and your repository:
Terminal window export FREIGHTAPI_KEY="sk_test_..."export FREIGHTAPI_BASE_URL="https://api.eftisandbox.app"Check that it works:
Terminal window curl -s "$FREIGHTAPI_BASE_URL/v1/shipments?limit=1" \-H "Authorization: Bearer $FREIGHTAPI_KEY"A
401with codeinvalid_api_keymeans the key does not exist or was revoked; see Error codes. -
Create your first shipment
Section titled “Create your first shipment”A shipment needs at least a consignor, a carrier, a consignee, a pickup address, a delivery address and goods. Everything beyond that is optional and fills out the eFTI dataset further.
Terminal window curl -s -X POST "$FREIGHTAPI_BASE_URL/v1/shipments" \-H "Authorization: Bearer $FREIGHTAPI_KEY" \-H "Content-Type: application/json" \-d '{"reference": "ORD-1042","transport_type": "international","consignor": {"name": "Demo Logistics B.V.","address": { "street": "Havenweg 12", "postal_code": "3089 JG", "city": "Rotterdam", "country": "NL" }},"carrier": {"name": "Transport Nowak Sp. z o.o.","address": { "street": "ul. Przemyslowa 8", "postal_code": "61-001", "city": "Poznan", "country": "PL" }},"consignee": {"name": "Mueller Maschinenbau GmbH","address": { "street": "Industriestrasse 4", "postal_code": "70565", "city": "Stuttgart", "country": "DE" }},"pickup": { "address": { "street": "Havenweg 12", "postal_code": "3089 JG", "city": "Rotterdam", "country": "NL" } },"delivery": { "address": { "street": "Industriestrasse 4", "postal_code": "70565", "city": "Stuttgart", "country": "DE" } },"goods": [{ "description": "CNC machine parts", "quantity": 12, "package_type": "pallet", "gross_weight_kg": 8400 }]}'import { FreightApi } from '@freightapi/sdk'const freight = new FreightApi({apiKey: process.env.FREIGHTAPI_KEY!,baseUrl: 'https://api.eftisandbox.app',})const shipment = await freight.shipments.create({reference: 'ORD-1042',transport_type: 'international',consignor: {name: 'Demo Logistics B.V.',address: { street: 'Havenweg 12', postal_code: '3089 JG', city: 'Rotterdam', country: 'NL' },},carrier: {name: 'Transport Nowak Sp. z o.o.',address: { street: 'ul. Przemyslowa 8', postal_code: '61-001', city: 'Poznan', country: 'PL' },},consignee: {name: 'Müller Maschinenbau GmbH',address: { street: 'Industriestraße 4', postal_code: '70565', city: 'Stuttgart', country: 'DE' },},pickup: { address: { street: 'Havenweg 12', postal_code: '3089 JG', city: 'Rotterdam', country: 'NL' } },delivery: { address: { street: 'Industriestraße 4', postal_code: '70565', city: 'Stuttgart', country: 'DE' } },goods: [{ description: 'CNC machine parts', quantity: 12, package_type: 'pallet', gross_weight_kg: 8400 },],})console.log(shipment.id, shipment.status)// The official .NET SDK arrives with M3-17. Until then Cargofollow is a plain REST API:// use HttpClient and System.Text.Json.using var http = new HttpClient { BaseAddress = new Uri("https://api.eftisandbox.app") };http.DefaultRequestHeaders.Authorization =new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FREIGHTAPI_KEY"));var response = await http.PostAsJsonAsync("/v1/shipments", shipmentCreate);response.EnsureSuccessStatusCode();var shipment = await response.Content.ReadFromJsonAsync<Shipment>();You get
201 Createdback with the full shipment:{"id": "shp_01M2FP3DE7YQ1Z24SBT4317SM2","mode": "test","status": "draft","version": 1,"current_version": {"id": "ver_01M2FP3DE7HZH0ENF0VH2CKXVB","version": 1,"document_hash": "54db818c1f898ad691916212ecba15582f56a9c4e3edef4850c5e62abc24da57"},"warnings": [{"path": "consignee.contact","code": "consignee_contact_for_delivery_signature","message": "Without an email address or phone number for the consignee no signature can be requested on delivery."}]}Keep the
id:Terminal window export SHIPMENT_ID="shp_01M2FP3DE7YQ1Z24SBT4317SM2" -
Issue the consignment note
Section titled “Issue the consignment note”While a shipment is
draftit changes freely. Issuing freezes the consignment note fields and lays the first link in the hash chain.Terminal window curl -s -X POST "$FREIGHTAPI_BASE_URL/v1/shipments/$SHIPMENT_ID/issue" \-H "Authorization: Bearer $FREIGHTAPI_KEY" \-H "Content-Type: application/json" \-d '{}'const issued = await freight.shipments.issue(shipment.id)console.log(issued.status, issued.current_version.document_hash)var issued = await http.PostAsJsonAsync($"/v1/shipments/{shipmentId}/issue", new { });issued.EnsureSuccessStatusCode();The status is now
issuedandissued_atis set. From here aPATCHon a frozen field returnsfield_frozen. -
Open the signing link
Section titled “Open the signing link”An issued shipment has an inspection link: a token URL with a QR code that a driver, a consignee or an enforcement officer can open without an account.
Terminal window curl -s "$FREIGHTAPI_BASE_URL/v1/shipments/$SHIPMENT_ID/inspection-link" \-H "Authorization: Bearer $FREIGHTAPI_KEY"{"token_id": "ins_01M2FP54YDY3DTNK4GQE1JPT8K","url": "https://sign.eftisandbox.app/i/CvtAOsHboY80GbORQNUOztOW3INHRSHGFd8kQ4FN4Dc","expires_at": "2027-09-14T10:07:12.845Z","qr_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...>"}Open
urlin a browser, or printqr_svgon the consignment note. Standalone PNG and SVG variants live at/v1/inspection-links/{token_id}/qr.pngand.../qr.svg. If a link leaks, revoke it withPOST /v1/shipments/{id}/inspection-link/rotate; the old token then returnstoken_rotated. -
Receive a webhook
Section titled “Receive a webhook”Instead of polling, subscribe an endpoint to events.
Terminal window curl -s -X POST "$FREIGHTAPI_BASE_URL/v1/webhook-endpoints" \-H "Authorization: Bearer $FREIGHTAPI_KEY" \-H "Content-Type: application/json" \-d '{"url": "https://example.com/hooks/freightapi","events": ["shipment.issued", "shipment.delivered"],"description": "Quickstart"}'{"id": "whe_01M2FP5AJCSSDQYQVAV865N8Q0","url": "https://example.com/hooks/freightapi","events": ["shipment.issued", "shipment.delivered"],"active": true,"mode": "test","secret": "whsec_0HBh8opWaFo6YUdG96sf8gPpk2ydhA4oxjVHuLGASSN"}Send yourself a test ping:
Terminal window curl -s -X POST "$FREIGHTAPI_BASE_URL/v1/webhook-endpoints/$ENDPOINT_ID/test" \-H "Authorization: Bearer $FREIGHTAPI_KEY"In your handler, verify the
freightapi-signatureheader before trusting the body:import { parseEvent } from '@freightapi/sdk/webhooks'const event = await parseEvent({secret: process.env.FREIGHTAPI_WEBHOOK_SECRET!,header: request.headers,// The body exactly as it arrived; never re-serialise the parsed JSON.body: await request.text(),})console.log(event.type, event.data)What happens when a delivery fails — retries, the dead-letter queue and replaying a delivery — is under Webhooks.
-
Let the sandbox drive the trip
Section titled “Let the sandbox drive the trip”In the sandbox you do not have to play driver yourself.
simulatewithaction: "auto"walks the whole lifecycle: signatures by consignor and carrier, in transit, delivered with the consignee’s signature.Terminal window curl -s -X POST "$FREIGHTAPI_BASE_URL/v1/test/shipments/$SHIPMENT_ID/simulate" \-H "Authorization: Bearer $FREIGHTAPI_KEY" \-H "Content-Type: application/json" \-d '{"action": "auto"}'Then follow what happened:
Terminal window curl -s "$FREIGHTAPI_BASE_URL/v1/shipments/$SHIPMENT_ID/events" \-H "Authorization: Bearer $FREIGHTAPI_KEY"Every event carries a
seq, ahashand theprev_hashof its predecessor. Whether that chain holds is whatGET /v1/shipments/{id}/integrityrecomputes.
- API reference — every route, callable against the sandbox.
- Error codes — every stable
codethe API returns. - SDKs — the TypeScript client and what is still coming.