Skip to content

Sandbox

The sandbox runs the same code as production: the same validation, the same state machine, the same hash chain, the same events and webhooks. What differs: nothing reaches real providers, nothing is billed, and you are allowed to simulate.

Base URL https://api.eftisandbox.app
Key prefix sk_test_
Console console.eftisandbox.app
OpenAPI https://api.eftisandbox.app/openapi.json

Test and live data are fully separate. An sk_test_ key never sees live shipments, and the other way round; an id from one mode does not exist in the other. Calling a live route with an organisation that is not cleared for it returns live_not_enabled. The reverse holds too: the simulation routes exist only in test mode and answer an sk_live_ key with test_mode_only.

In the real world a driver signs on a phone and a consignee signs at the loading dock. In the sandbox POST /v1/test/shipments/{id}/simulate plays those roles for you, so you can test your integration without sending anybody a link.

Action Body What happens
sign { "action": "sign", "role": "carrier" } Records a signature for that role, with method api, signer “Sandbox <role>” and evidence.simulated: true
deliver { "action": "deliver" } Delivers, including any signatures still needed for it
remark { "action": "remark", "type": "damage" } Adds a reservation
auto { "action": "auto" } Runs the whole lifecycle through to delivered

auto is the interesting one: it starts a background process that issues the shipment, has the carrier sign a few minutes later, and then has the consignee sign with a reservation — each step through the same services as a real shipment. So you get the same events, the same PDFs and the same webhooks as live. GET /v1/test/shipments/{id}/simulation shows the progress.

Want that automatically on every new shipment? Set metadata.sandbox_auto: true on creation and auto starts by itself after issuing.

Three lanes that each show something different. They are chosen so the rule register reacts differently to each; see Country requirements.

Both countries are parties to the e-CMR Protocol, so no paper is involved and there are no blocking findings.

Terminal window
curl -X POST https://api.eftisandbox.app/v1/shipments \
-H "Authorization: Bearer $FREIGHTAPI_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"reference": "DEMO-NL-DE",
"consignor": { "name": "Van Dijk Logistiek", "address": { "line1": "Havenweg 12", "postal_code": "3089 JH", "city": "Rotterdam", "country": "NL" } },
"carrier": { "name": "Sandbox Transport", "address": { "line1": "Industrieweg 4", "postal_code": "5222 AL", "city": "s-Hertogenbosch", "country": "NL" } },
"consignee": { "name": "Muller Handel GmbH", "address": { "line1": "Hafenstrasse 7", "postal_code": "47119", "city": "Duisburg", "country": "DE" } },
"pickup": { "address": { "line1": "Havenweg 12", "postal_code": "3089 JH", "city": "Rotterdam", "country": "NL" } },
"delivery": { "address": { "line1": "Hafenstrasse 7", "postal_code": "47119", "city": "Duisburg", "country": "DE" } },
"goods": [{ "description": "Palletized machine parts", "packages": 8, "package_type": "pallet", "gross_weight_kg": 1840 }]
}'

Belgium never ratified the e-CMR Protocol. Change country on the consignee and the delivery address to BE and you get an extra finding: the Benelux pilot asks for a provider recognised by NIWO. Leave provider out and routing picks one for you.

Set adr: true on a goods line and POST /v1/shipments/validate asks for the UN number, the proper shipping name, the hazard class and the packing group. Handy for testing your error handling: you get a 422 with an errors array per field.

Terminal window
curl -X POST https://api.eftisandbox.app/v1/shipments/validate \
-H "Authorization: Bearer $FREIGHTAPI_KEY" \
-H "Content-Type: application/json" \
-d '{ "goods": [{ "description": "Paint", "packages": 2, "gross_weight_kg": 40, "adr": true }] }'

Before you may go live, we want to see that your integration handles the things that go wrong in production — not just the happy path. The console therefore tracks per organisation which scenarios you have demonstrated in test mode:

Scenario What you demonstrate
create_shipment Creating a shipment
validate_with_error_handled Receiving a 422 and then doing a valid create
issue Issuing
signature_via_link A signature through a signing link
delivery_with_pod Delivering with an ePOD
webhook_endpoint_configured An endpoint configured
webhook_echo_verified A ping answered with a correct freightapi-echo header
webhook_delivery_succeeded A real delivery with a 2xx
idempotency_key_used Idempotency-Key used on a POST
integrity_checked The hash chain requested
pdf_downloaded A PDF downloaded
auto_sandbox_run A shipment run through auto

Once all twelve scenarios are ticked, your organisation is ready_for_live and you can request live access in the console. Clearing it stays a human act; nothing switches on automatically.

  • Real providers. External eCMR platforms are never called; the mock provider pretends. A real provider integration is only exercised in production.
  • Billing and limits. Rate limits are lower in test (600 per minute against 1,200 live), so a load test in the sandbox says nothing about live.
  • Retention. Sandbox data may be cleaned up. Do not store anything there you need.