Skip to content

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.

  1. 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 401 with code invalid_api_key means the key does not exist or was revoked; see Error codes.

  2. 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 }
    ]
    }'

    You get 201 Created back 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"
  3. While a shipment is draft it 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 '{}'

    The status is now issued and issued_at is set. From here a PATCH on a frozen field returns field_frozen.

  4. 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 url in a browser, or print qr_svg on the consignment note. Standalone PNG and SVG variants live at /v1/inspection-links/{token_id}/qr.png and .../qr.svg. If a link leaks, revoke it with POST /v1/shipments/{id}/inspection-link/rotate; the old token then returns token_rotated.

  5. 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-signature header 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.

  6. In the sandbox you do not have to play driver yourself. simulate with action: "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, a hash and the prev_hash of its predecessor. Whether that chain holds is what GET /v1/shipments/{id}/integrity recomputes.

  • API reference — every route, callable against the sandbox.
  • Error codes — every stable code the API returns.
  • SDKs — the TypeScript client and what is still coming.