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.
Basics
Section titled “Basics”| 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.
Simulating
Section titled “Simulating”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.
Example shipments
Section titled “Example shipments”Three lanes that each show something different. They are chosen so the rule register reacts differently to each; see Country requirements.
The simple case — NL to DE
Section titled “The simple case — NL to DE”Both countries are parties to the e-CMR Protocol, so no paper is involved and there are no blocking findings.
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 }] }'The Belgian case — NL to BE
Section titled “The Belgian case — NL to BE”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.
The ADR case — dangerous goods
Section titled “The ADR case — dangerous goods”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.
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 }] }'Conformance checklist
Section titled “Conformance checklist”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.
What the sandbox does not test
Section titled “What the sandbox does not test”- Real providers. External eCMR platforms are never called; the
mockprovider 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.