Rendered from docs/INTEGRATOR-GUIDE.md. The interactive reference is Swagger UI; the seven request examples are attached there and on the portal's API & integration page.
Freightools GPT — Integrator Guide #
For the developer who connects a forwarder's system to Freightools GPT. You send one JSON per shipment (an import to Israel, an export from Israel or a cross-trade); the service validates it completely, creates or updates the Customer Shipment (CS) and Shipping Order (SO) in Freightools, waits for the external prerequisites, transmits the Israeli customs sub-manifest when you asked for it, and returns the customs feedback. The same information is shown in the customer portal.
This guide is the customer-facing rendering of the approved plan (docs/PLAN-2026-11-04.md §3, §4.6, §4.8, §5, §6). The interactive reference is Swagger UI at /docs (OpenAPI at /openapi.json).
Evidence labels used below, inherited from the plan: VERIFIED (seen in a live read-back, the Swagger or a real customs file), INFERRED (pattern read from samples), UNVERIFIED (assumption), FT-DEPENDENT (only the Freightools backend or a real example can settle it). Where behaviour is FT-DEPENDENT we say so; design your integration so that it tolerates the states in §9 rather than assuming success.
1. Authentication #
Every API call carries an API key as a bearer token:
Authorization: Bearer em_test_<keyId>.<secret>
Authorization: Bearer em_live_<keyId>.<secret>
- Keys are environment-bound. A key is created inside one tenant environment (
testorproduction) and only ever touches that environment's Freightools host. Anem_test_…key can never create anything on production and vice versa. The prefix tells you which one you hold; the server checks the binding on every request. - Shown once. The secret is displayed a single time when the key is generated in the portal (Settings → API keys). We store only a SHA-256 hash. If you lose the secret, generate a new key and revoke the old one (rotation = create + revoke; both keys work during the overlap).
- Scopes. A key carries a subset of:
shipments:read,shipments:write,manifest:send,documents:read,documents:write,logs:xml,org:read. A call outside the key's scopes returns403with the standard envelope.logs:xmlis the only scope that returns raw customs XML and every such read is written to the tenant audit log. - IP allow-list (optional, per key): requests from other addresses are refused with
403. - Missing or invalid token:
401with the JSON envelope (§6), never an HTML page.
The portal (/portal) uses user accounts (e-mail + password, optional TOTP), not API keys. Portal users generate and revoke API keys.
2. The one call: POST /v1/shipments #
POST /v1/shipments
Authorization: Bearer em_test_…
Content-Type: application/json
Prefer: wait=20 (optional, ≤ 20 s, see below)
Body: the shipment JSON (§3). Body limit 1 MiB (413 payload_too_large).
2.1 Responses #
| HTTP | Meaning |
|---|---|
201 Created | New shipmentRef: a shipment and revision 1 were created and the apply (Freightools writes) is queued or done. Location and ETag headers. |
202 Accepted | Known shipmentRef, changed data: a new revision was created and is being applied. |
200 OK + header Idempotent-Replayed: true | Known shipmentRef, identical data: nothing was written anywhere. The current representation is returned. |
400 / 422 | Validation failed (§6). Nothing was written; the previous revision, if any, stays current. |
503 tenant_not_configured | The environment has no Freightools license key yet; nothing is validated or recorded. Ask your Freightools contact. |
A closed write gate does not change the status: the shipment is validated and recorded (201/202) and the apply stops before the first Freightools call — processing.state is blocked and processing.readiness.checks carries send_gates = blocked with the detail "Freightools writes are disabled for this environment (switched on by Freightools support)". The apply resumes by itself when the gate is opened. 503 gate_closed is answered only by POST …/manifest (§8), never by this POST.
The body of 200/201/202 is the shipment representation (§7). With Prefer: wait=N (N ≤ 20 seconds; larger values are clamped to 20) the server waits up to N seconds for the Freightools apply to finish before answering, so a quick integration can see freightools.customerShipment.code in the first response. Without it, poll GET /v1/shipments/{id}.
2.2 Same shipmentRef = same shipment #
shipmentRef is your key (^[A-Za-z0-9._/-]{1,60}$, unique per tenant environment). Sending the same reference again never creates a second shipment and never returns 409. What happens depends on what changed:
| You send | What happens | Response |
|---|---|---|
Exactly the same data and the same submitManifest | Identical retry. A submission row is recorded; no revision, no Freightools write, no transmission. Whitespace, key order and number formatting differences (11500 vs 11500.0) count as identical, because the comparison is a hash of the normalised body. | 200 + Idempotent-Replayed: true |
Same data, submitManifest flipped | Intent revision. Nothing is written to Freightools. false → true arms the automatic send (if the shipment is ready); true → false disarms it and cancels a send that is queued but not started. A send that has already called Freightools is never cancelled. | 202 |
| Changed data (also a change back to an older revision) | Data revision, applied immediately — before a transmission and after one. Freightools is updated right away. If a customs transmission already exists, the manifest state becomes updated_not_retransmitted and a customs_decision action is opened (§10): nothing is retransmitted, corrected or cancelled at customs without your confirmation. | 202 |
| Invalid body | A submission row with the errors is recorded for the portal's Validation tab; no revision; the current revision stays as it was. | 400 / 422 |
Two simultaneous POSTs for the same reference are serialised: you get either an identical replay or revisions N+1 and N+2, never an error.
Changes that Freightools cannot express as an update return 422 unsupported with unsupported_change — in release 1 that is a change of service variant (FCL ↔ LCL ↔ air). Use a new shipmentRef.
options.dryRun: true runs the full validation and resolution and returns the warnings and known deviations, but writes nothing and creates no revision.
2.3 Free shipments and the payment method #
A company's first 10 production shipments are free (pay as you go, see /pricing). After them, while the company is still on the free trial, a request that would create a new shipment in production answers 402 payment_method_required; nothing is validated or recorded:
{ "type": "payment_method_required", "status": 402, "requestId": "req_01JC…", "shipmentRef": "EXAMPLE-FCL-011",
"message": "Your first 10 shipments were free. Add a payment method to create more.",
"errors": [ { "path": "", "code": "payment_method_required", "message": "Your first 10 shipments were free. Add a payment method to create more.",
"hint": "An owner or admin of your company adds it in the portal: Settings → Usage & billing. https://<portal>/portal/production/settings/usage" } ],
"settingsUrl": "https://<portal>/portal/production/settings/usage" }
An owner or admin of your company adds the payment method in the portal under Settings → Usage & billing; once Freightools has set it up, send the same body again. Never refused: a new revision or an identical retry of an existing shipmentRef, options.dryRun, the test environment (it never counts), customs sends, decisions and documents. What counts is every production shipment created through this API, the portal forms or the copilot, once; shipments synced from Freightools never count.
3. The shipment JSON #
3.1 Structure #
Common core plus blocks whose shape depends on service. A block of the wrong shape for the mode is 400 not_applicable_for_variant.
shipmentRef, submitManifest? all
service { mode: ocean|air, load: FCL|LCL (ocean), scope?, lcl?: { arrangement: lineMaster|coload|consolidation, consolidationRef? } }
parties { shipper?, consignee?, notify?, customer?, customsBroker?, coloader? (co-load only) }
the customer must be among them: the consignee, the shipper or customer.details (§3.1.1)
dgContact? when any goods line is dangerous and the tenant has no default
routing ocean: { pol, pod, placeOfReceipt?, placeOfDelivery?, acceptancePlace?, transshipmentPorts[]? }
pod: an import may give master.dischargeSite instead
air: { aol, aod, acceptancePlace?, placeOfReceipt?, placeOfDelivery?, transshipmentAirports[]? }
carriage? ocean: { carrier?{scac|name}, vessel?{imo,name}, voyage?, eta?, etd?, booking?, bookingDate?, pickupDate?, cutoffDate?, transshipments[]? }
air: { airline?{iata|icao}, flight?{number,date}, eta?, booking?, bookingDate?, pickupDate?, cutoffDate? }
master? { issuer?: carrier|coloader|self, document?{number?,type?}, loadingPort?, dischargeSite?, goodsReceiptSite?, transactionNumber?, manifestNumber? }
house? { document?{number?,type?}, placeOfIssue?, transactionNumber? }
commercial?{ incoterm?, freightTerms?, freightCharge? (air) }
cargo FCL: { containers[]{ number?, isoType, seal?, grossKg?, cbm?, tareKg?, soc?, reefer?, packages?{count,type,marks?}, goods[] }, totalCbm? }
LCL: { container?{ number?, isoType, seal? }, packages[]{ count, type, grossKg?, cbm?, marks?, goods[] } }
AIR: { pieces[]{ count, type, grossKg?, cbm?, dimensionsCm?, chargeableKg?, goods[] } }
goods[] { hs?, description, additionalHs[]?, dangerous?{ unNumber, class, packingGroup?, properShippingName? }, vehicles[]?{vin}, value? }
options { dryRun? }
A ? marks what may be left out. Unknown fields are rejected (unknown_field) so that a typo never silently drops data. Documents are not part of this JSON (§12).
Variants: F ocean FCL · L ocean LCL under a line master · C ocean LCL co-load under another forwarder's house · K ocean LCL consolidation (gated until Freightools evidence set B1 is in hand: 422 consolidation_unsupported) · A air import house.
3.1.1 Start small #
A shipment does not have to be complete to be created. Five things are enough; everything else can come in later revisions with the same shipmentRef (§2.2):
| What | Fields | Notes |
|---|---|---|
| Your reference | shipmentRef | your key; every later revision carries the same one |
| The service | service.mode; ocean also service.load; LCL also service.lcl.arrangement | chooses the variant (F, L, C, K, A) |
| The two ends | ocean routing.pol + routing.pod; air routing.aol + routing.aod | they decide the trade (§3.2.1); an import may give master.dischargeSite instead of routing.pod |
| The cargo | FCL cargo.containers[] with isoType and goods[].description; LCL cargo.packages[] with count, type and goods[].description; air cargo.pieces[] the same | no weight, container number, seal or HS code is needed yet; an LCL's shared container (cargo.container) may come later too |
| The customer | parties.consignee on an import or cross-trade, parties.shipper on an export, or someone else as parties.customer (§3.2.1) | a name and a country are enough; a body without its customer is 422 customer_required |
Nothing else is required to create a shipment (a co-load, variant C, also names parties.coloader). What you do send is validated as always: an import's consignee must be in Israel, a tax id must have the right form, a booking number needs its carriage.etd, dangerous goods need their DG contact.
The smallest FCL import:
{
"shipmentRef": "EXAMPLE-START-001",
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"consignee": {
"name": "Example Imports Ltd",
"country": "IL"
}
},
"routing": {
"pol": "CNSZX",
"pod": "ILASH"
},
"cargo": {
"containers": [
{
"isoType": "45G1",
"goods": [
{
"description": "OFFICE FURNITURE"
}
]
}
]
}
}
The shipment is created (201). The trade is import (discharged in Ashdod), the consignee is the customer, submitManifest left out means false, and the incoterm, the loading port and the place of acceptance are filled in and reported as warnings (§6.3). A consignee that is not yet a Freightools partner is created with - as its address, city and phone (warnings partner_address_placeholder, partner_tel_placeholder) until a revision brings them.
A later revision completes it: the same shipmentRef and the body again, now with what has become known — here the shipper, the carrier and its dates, the weight, the importer's tax id and address, and the customs discharge site:
{
"shipmentRef": "EXAMPLE-START-001",
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Trading Co., Ltd",
"address": "12 Harbour Road",
"city": "Shenzhen",
"country": "CN"
},
"consignee": {
"name": "Example Imports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Tel Aviv-Yafo",
"country": "IL"
}
},
"routing": {
"pol": "CNSZX",
"pod": "ILASH"
},
"carriage": {
"carrier": {
"scac": "MSCU"
},
"etd": "2026-12-01",
"eta": "2026-12-20"
},
"master": {
"dischargeSite": "ILAST"
},
"cargo": {
"containers": [
{
"isoType": "45G1",
"grossKg": 11500,
"goods": [
{
"description": "OFFICE FURNITURE"
}
]
}
]
}
}
What an import's customs manifest needs is asked later, not at create: the shipment is created and its readiness check manifest_data (§9.2) stays waiting, naming what is still missing — the consignee and its tax id, the shipper, their addresses, the customs discharge site (LCL: also the goods-receipt site), the weights, container numbers, seals, packages, HS codes and the master / house document numbers. Customs receives the Freightools partner's data, so a tax id or an address the shipment's Freightools partner already holds is not asked for again. After the second body above it still names the container number, the seal, the packages, the HS code and the two B/L numbers; they come in further revisions. An export or cross-trade has no Israeli customs manifest and is ready once applied.
3.2 How to read the field tables #
Columns:
- F / L / C / K / A — requirement per variant: R required, O optional, C conditional (the condition is in the Validation column), – not applicable (sending it is
not_applicable_for_variant). - Validation — what is checked before anything is written. Numbers in brackets (e.g. 553, 1023) are the Israeli customs check codes the rule protects you from.
- Freightools receives — the CS/SO/Partner field the value is written to (Freightools is where the manifest XML is generated from).
- Customs receives — whether the value actually reaches the customs manifest today. This is the honest part:
- YES-V — seen emitted from this field in a real manifest XML.
- YES-I — emitted, but the source field is inferred rather than seen.
- NO — not emitted today, or there is no Freightools field for it. The value is stored and may be appended to a description, but that is not transmission. Every such value is listed back to you in
knownDeviations[](§6.4) with the Freightools change request (CR-n) that would fix it. - FTD — air: there is no customs tool on the air process in Freightools today, so nothing air reaches customs through this path yet; the customs element the value would feed is named so you know what the data is for.
- N/A — the field is for Freightools only and has no customs meaning.
3.2.1 Import, export and cross-trade (and the booking stage) #
Every shipment is one of three trades, worked out from the two ends of the main carriage (routing.pol → routing.pod, or routing.aol → routing.aod; an import's master.dischargeSite stands for an Israeli POD):
| Trade | The route | The Israeli party | Israeli customs |
|---|---|---|---|
import | discharged in Israel, loaded abroad | parties.consignee: when given, its country must be IL (consignee_country_not_il); its taxId is not needed to create — the customs manifest needs it | the customs sub-manifest, readiness and the automatic send apply. None of its data is needed to create: submitManifest left out is false, and the manifest waits for master.dischargeSite and whatever else it lacks (manifest_data, §3.1.1). routing.pod is required unless master.dischargeSite is given (the port then follows from it) |
export | loaded in Israel, discharged abroad | parties.shipper: when given, its country must be IL (shipper_country_not_il); its taxId is optional | none: master.dischargeSite, master.goodsReceiptSite, the transaction / manifest numbers and submitManifest: true are refused (not_applicable_for_trade, manifest_not_applicable); routing.pod is required |
crossTrade | neither end in Israel | none | none (as export); routing.pod is required |
service.trade may state the trade; when it is given it must agree with the ends (trade_conflict). Both ends in Israel is refused (trade_domestic). A body without service.trade (every body written before this section existed) keeps its meaning: discharged in Israel = import. Freightools receives the trade as ServiceTrade on the customer shipment and the shipping order (0 export, 1 import, 3 cross-trade).
Booking stage. A shipment can be created before its container numbers, seals, packages, HS codes and B/L (AWB) numbers exist — and before its weight, its carrier and its dates (§3.1.1): send the container type and the goods descriptions, and send the rest in a later revision with the same shipmentRef. A booking line numbered later updates its own Freightools goods row. An import's customs manifest needs them: until they are there its readiness check manifest_data stays waiting and names what is missing. An export or cross-trade never waits for them.
Customer and incoterm. The customer is the one party a shipment cannot be created without (Freightools does not save a customer shipment without its customer). It is parties.customer.party when stated (other needs parties.customer.details; details alone means other). Left out, it is the importer (consignee) on an import or cross-trade and the exporter (shipper) on an export — on an import, the environment's default paying party when one is set — when that party is given; otherwise the only one of shipper and consignee that is given. A body whose customer is not there is refused with 422 customer_required, reported at the missing party's name (/parties/consignee/name, /parties/shipper/name or /parties/customer/details/name). commercial.incoterm left out is derived with Freightools' Instant Quote rule and reported as warning incoterm_derived: a customer on the importer side gets EXW with a pickup door (service.scope starting at door), else FOB (air FCA); on the exporter side DAP with a delivery door, else CFR (air CPT). Freight terms follow the incoterm as before.
Tracking. tracking: { "active": true|false, "alerts"?: true|false } switches Freightools tracking and alerts on the shipping order (ActiveTracking / ActiveAlerts; alerts follows active when left out). Left out, a new shipment follows the company setting "Track new shipments" (on unless switched off) and an update leaves Freightools' value alone.
3.3 Header and service #
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
shipmentRef | R | R | R | R | R | ^[A-Za-z0-9._/-]{1,60}$; unique per tenant environment | CS.Comment marker [EM:<tenant>:<ref>:r<n>]; our ledger. Not the Freightools customer reference (that is parties.customer.reference) | N/A |
submitManifest | O | O | O | O | O | boolean; left out on an import it is false (nothing is sent to customs unasked); refused as true on an export or cross-trade (manifest_not_applicable). Intent only; not part of the data hash | drives the automatic send | N/A |
service.mode | R | R | R | R | R | ocean | air | Freightools service type / mix is decided for you (FCL mix 2, LCL mix 3, air mix 1) | — |
service.load | R | R | R | R | – | FCL | LCL | MovementType 27 (FCL) / 30 (LCL) / null (air) | condition code 719 YES-V (27 and 30 seen) |
service.lcl.arrangement | – | R | R | R | – | lineMaster | coload | consolidation; must agree with master.issuer | decides the expected IL2 format (line master …00 vs house) | — |
service.lcl.consolidationRef | – | O | O | O | – | ≤ 60 | stored | N/A |
service.trade | O | O | O | O | O | import | export | crossTrade; worked out from the ends when left out; must agree with them (trade_conflict) — §3.2.1 | CS + SO ServiceTrade 1 / 0 / 3 | import only |
service.scope | O | O | O | O | O | port-port (default) | door-port | port-door | door-door | StartFrom/ArriveTo + ORIGIN/DESTINATION legs (INFERRED encoding) | N/A |
3.4 Parties #
A party object: { name, address?, city?, country, zip?, phone?, email?, partnerCode?, taxId?, taxIdType? } — a name and a country are enough; the address and the city may come later. Limits: name ≤ 70, address ≤ 70, city ≤ 35, zip ≤ 10, phone ≤ 50 with ≥ 6 digits, e-mail ≤ 100, partnerCode ≤ 50, taxId ≤ 20, taxIdType ∈ vat | company | passport | other. Country is ISO 3166-1 alpha-2. Placeholder addresses (NA, N/A, DD, CC, 000, 0, -, C) are rejected (placeholder_address): leave out an address or a city you do not have.
parties.shipper and parties.consignee are both optional; what a shipment needs is its customer (§3.2.1, customer_required), so each of the two is required only when it is the customer.
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
parties.shipper {name, address, city, country} | C | C | C | C | C | required when the shipper is the customer (customer_required), optional otherwise; inside: name and country required, address and city optional. On an export the shipper is the Israeli exporter: country IL (shipper_country_not_il); its taxId is optional. An import's customs manifest needs the shipper (manifest_data) | Partner (Name/LegalEntity/Address/City/CountryId) → CS.ShipperPartnerId; a partner without an address or a city gets "-" there (warning partner_address_placeholder) | consignor 500/506/502/503 YES-V · A: FTD |
parties.shipper {zip, phone, email, partnerCode, taxId, taxIdType} | O | O | O | O | O | phone (1013); Freightools requires Partner.Tel: when absent "-" is written with warning partner_tel_placeholder; partnerCode → Partner.InternalIdentity only when supplied | Partner.ZipCode/Tel/Email/InternalIdentity | postcode YES-V; consignor id field UNKNOWN (FT-DEPENDENT, CR-8); communication NO |
parties.consignee {name, taxId, address, city, country} | C | C | C | C | C | required when the consignee is the customer (customer_required), optional otherwise; inside: name and country required, taxId, address and city optional. Import: country must be IL (consignee_country_not_il); taxId is not needed to create — the customs manifest needs the consignee and its tax id (manifest_data) — and when given it is 9 digits or a passport <docType><CC><number> with CC ≠ IL (553) and must differ from the tenant's submitter id under condition 27 (412). Export / cross-trade: any country; a foreign party's own tax id is not held to the Israeli format | Partner (Name/VAT/Address/City/CountryId) → CS.ConsigneePartnerId; "-" for a missing address or city, as for the shipper | consignee 312/311/318/314/315 + importer statement 2 YES-V · A: FTD |
parties.consignee {zip, phone, email, partnerCode} | O | O | O | O | O | as shipper | Partner fields | postcode YES-V; communication NO |
parties.notify | O | O | O | O | O | party object | CS.NotifyPartnerId | NO (never emitted) → NOTIFY_NOT_EMITTED |
parties.customer {party, reference, details} | O | O | O | O | O | party ∈ shipper | consignee | other (then details, a party object, is required; details alone means other); a stated shipper or consignee must be given in parties; left out: the consignee (on an export the shipper) when it is given, else the other of the two — §3.2.1; no customer → customer_required; reference ≤ 50 | CustomerFromTo + CustomerReceiver/SenderPartnerId + Customer*Referance | N/A |
parties.customsBroker | O | O | O | O | O | party with taxId; resolves to a partner (role filter UNVERIFIED) | CS.CustomsOperatorId (VERIFIED field) | NO (delivery-order hook for a later phase) |
parties.coloader | – | – | R | – | – | party object | Freightools field UNVERIFIED (CS.ForwarderPartnerId / SO.AgentPartnerId candidates); stored → COLOAD_MASTER_FIELDS_UNVERIFIED | N/A |
dgContact {company, person, phone, phoneType} | C | C | C | C | C | required when any goods line is dangerous and the tenant has no default DG contact (dg_contact_required, customs 602); phoneType ∈ TE | AL | FX | EM (default TE) | no Freightools field | NO (UNDGContact 37B) → DG_CONTACT_NOT_EMITTED (CR-1b) |
Partner matching (how a party becomes a Freightools partner): partnerCode first, then taxId against the tenant's partner mirror, then exact normalised name + country, else a new partner is created. Freightools requires an address, a city and a phone on a partner: a new partner created from a party without them gets "-" in those fields, with warnings partner_address_placeholder and partner_tel_placeholder, until a revision brings them; nothing an existing partner holds (tax id, address, city, phone, postcode, e-mail) is ever overwritten by an empty value. A code that resolves to a partner whose taxId or name differ is 422 party_code_tax_id_mismatch. A match whose address or phone differ produces warning party_details_differ_from_partner — customs receives the Freightools partner data, not your JSON. A partner shared with other Freightools users is never rewritten silently: non-additive differences produce warning partner_data_diverges.
3.4.1 Partner codes #
partnerCode is the party's id in your own system — the code your ERP or accounting system gives the customer, shipper or consignee. Freightools keeps it on the partner as its Internal Identity: the bridge between your system and Freightools.
- A new partner gets your code. A party that matches no Freightools partner is created as a new partner; with
partnerCodegiven, its Internal Identity is that code. - An existing partner without a code gets yours the first time a shipment names it. A partner that already has another code keeps it (partners are shared with other Freightools users and are only ever filled in, never overwritten); the response then carries warning
partner_data_diverges. - How a party is matched. We keep a copy of your environment's Freightools partners and match every party against it, in this order:
partnerCode, thentaxId, then the exact name (letters and digits only, any case) together with the country. The first rule that finds a partner decides. Several partners for one rule is422 partner_ambiguous; apartnerCodethat finds a partner with anothertaxIdor name is422 party_code_tax_id_mismatch. - When a code starts to count. A code given to a new partner, filled into an existing partner by a shipment, or set on the portal's Partners page matches at once. A code changed directly in Freightools matches after the next sync of the partner list (every night, or Sync from Freightools on the Partners page); until then the partner is still found by its tax id or name.
- One code, one partner. Send the same code for the same partner every time. The portal's Partners page lists every partner with its code and Freightools ID, and operators can set or change a code there — it is written to Freightools, so it needs the environment's Freightools writes switched on, and a code another partner of the environment already has is refused.
"consignee": { "name": "Example Imports Ltd", "taxId": "510000052", "partnerCode": "EXT-CONS-0007", "address": "2 Example Street", "city": "Haifa", "country": "IL" }
3.5 Routing #
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
routing.pol | R | R | R | R | – | sea-port UN/LOCODE, resolved as a sea port | CS POL leg + CS.MasterPhysicalAddressFromId + CountryFromId; never the SO | NO (the SO master port reaches the XML, not the CS port) |
routing.pod | C | C | C | C | – | required for every ocean shipment (422 required at /routing/pod), except that an import may give master.dischargeSite instead: the port then defaults from it via the tenant site→port table (warning pod_defaulted_from_site). Import: an Israeli sea-port LOCODE | CS POD leg + CS.MasterPhysicalAddressToId; export / cross-trade, and an import while it has no master.dischargeSite, also SO.MasterAddressToId | NO (same) |
routing.aol / routing.aod | – | – | – | – | R / R | airport LOCODEs (resolved as airports; airports and sea ports share LOCODEs but are different Freightools records) | AOL/AOD legs + CS master addresses + SO master from/to | FTD: GoodsConsignedPlace 565 (aol) |
routing.acceptancePlace | O | O | O | O | O | free text ≤ 256; default, sea and air: placeOfReceipt city, else the port (air: the airport) of loading (warning acceptance_place_defaulted) | no Freightools field; the sea manifest emits the POL country code here | sea: element emitted but not your value → ACCEPTANCE_PLACE_IS_COUNTRY_CODE (CR-11) · A: FTD 209 |
routing.placeOfReceipt / routing.placeOfDelivery | C | C | C | C | C | required when scope starts/ends at door (door_address_required); {country, city, zip?, address} | ORIGIN / DESTINATION legs as text | N/A |
routing.transshipmentPorts[] / transshipmentAirports[] | O | O | O | O | O | LOCODEs, ≤ 5 | TRN legs | N/A |
3.6 Carriage #
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
carriage.carrier.scac (or .name) | O | O | O | O | – | optional: the carrier can come later, with the booking; SCAC ^[A-Z]{4}$ (key); name informational; both given and disagreeing → 422 carrier_conflict; name with several hits → carrier_ambiguous | SO.SystemCarrierId + SO.CarrierPartnerId: the company's partner for that carrier; when the company has none, one is created (warning carrier_partner_created) | N/A |
carriage.vessel {imo, name} | O | O | O | O | – | IMO 7 digits (key); a name alone is matched in the vessel book, then in Freightools. Never an error: every vessel finding is a warning and an unmatched vessel leaves the SO without one (see Vessel below) | SO.EquipmentId when the vessel is matched (which vessel the Freightools lookup needs when there are transshipments is UNKNOWN, R4) | N/A |
carriage.voyage | O | O | O | O | – | ≤ 20; left out, Freightools fills it from the carrier's manifest | SO.Voyage | N/A |
carriage.eta / etd | O / C | O / C | O / C | O / C | O / – | YYYY-MM-DD; both can come later; etd is required when carriage.booking is given (etd_required_with_booking); eta ≥ etd (eta_before_etd) | SO.ShippingOrderDates.Estimated* | N/A |
carriage.booking | O | O | O | O | O | ≤ 35 | SO.BookingNumber | N/A |
carriage.bookingDate | O | O | O | O | O | YYYY-MM-DD; left out, the day the order first gets its booking number | SO.BookingDate | N/A |
carriage.pickupDate / cutoffDate | O | O | O | O | O | YYYY-MM-DD | SO.ShippingOrderDates.EstimatedPickup / EstimatedCutoff (confirmed with the other dates) | N/A |
carriage.transshipments[] | O | O | O | O | – | {port, vessel?, voyage?, eta?}, ≤ 5 | TRN leg + SO.ShippingOrderTransshipments (writability UNVERIFIED) | N/A |
carriage.airline {iata | icao} | – | – | – | – | O | optional: the airline can come later, with the booking; when given, the resolved airline's AWB prefix must equal master.document.number[0:3] → airline_awb_prefix_mismatch; iata and icao both given and disagreeing → airline_conflict | SO.SystemCarrierId/CarrierPartnerId (absent on the air SOs seen) | FTD: Carrier.ID 182 + Declaration.ID prefix |
carriage.flight {number, date} | – | – | – | – | O | optional: the flight can come later; when given, both number and date; designator stripped and checked against the airline (flight_airline_mismatch); date = scheduled departure of the last leg before Israel, not the ETA | SO.Voyage <IATA><number>, SO.ShippingOrderDates.EstimatedDeparture | FTD: Declaration.ID chars 4–15 |
The whole carriage block may be left out: the carrier (air: the airline and the flight) and the dates come with the booking, in a later revision.
Vessel #
The vessel and the voyage are optional for every ocean variant, and a vessel never stops a shipment. When you leave them out, the shipping order is created without them and Freightools fills the vessel, the voyage and the manifest number from the carrier's manifest about 7 days before arrival. When you give them:
- IMO number (preferred). Seven digits, the last a check digit. The IMO is the key: we find the ship in Freightools by it and write it to the shipping order. A name that is not the ship's current or a former name is reported (
vessel_conflict) and the IMO is used. - Name only. Matched in our vessel book first (current and former names, so a renamed ship is still found;
M/V, case and punctuation do not matter), then by exact name in Freightools. When the name matches one ship, the warningvessel_matched_by_nametells you which IMO was used.
"carriage": { "carrier": { "scac": "MSCU" }, "vessel": { "imo": "9001019", "name": "EXAMPLE STAR" }, "voyage": "001E", "eta": "2026-12-20" }
| Warning | When | What happens |
|---|---|---|
vessel_imo_invalid | the IMO's check digit fails | the IMO is ignored; the name is used when there is one |
vessel_unknown | neither the vessel book nor Freightools knows the IMO or the name | the shipment goes ahead without the vessel; Freightools fills it |
vessel_ambiguous | the name matches several ships (an old ship and a new one often share a name) | no vessel is chosen; the message lists each ship's IMO and build year — give the IMO to pick one |
vessel_conflict | the name is not the IMO's | the IMO wins |
vessel_matched_by_name | a name alone matched one ship | its IMO (value) is used |
A renamed ship. Freightools may hold one vessel record per name under the same IMO. The shipping order gets the record that carries the name you sent; with the IMO alone, the record of the vessel's current name.
Dates already in the past. A pickupDate, cutoffDate, etd or eta that lies before today (Israel) when the shipment is written is stored as done: the shipping order gets the actual date and its stage moves to Pick Up, Cut Off, Departure or Arrival. Only dates this request sets are treated so, and a date of today is not. An arrival is stored only on an order that has its actual departure (from this request or already in Freightools).
Transshipment legs (carriage.transshipments[].vessel) take the same {imo, name}; a leg's IMO with a wrong check digit is reported as vessel_imo_invalid and ignored. The examples in §4 carry a vessel and a voyage because most bookings know them; both may be left out.
3.7 Master and house documents #
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
master.issuer | O (carrier) | O (carrier) | R (coloader) | R (self) | O (carrier) | must agree with service.lcl.arrangement (issuer_arrangement_mismatch) | none directly | decides the expected IL2 format |
master.document {number, type} | O / O | O / O | O / O | O / O | O / – | sea ≤ 35; MAWB ^\d{3}-?\d{8}$ (invalid_mawb); type ∈ original | waybill | telex → MBLType (UNVERIFIED encoding); same number as house.document → master_equals_house | SO.MasterBillOfLading (+ MBLType); left out, the order keeps what it holds (booking stage) | 704 YES-V (a co-loader's HBL is also sent as 704) · A: FTD 741; an import's manifest waits for it (manifest_data) |
master.loadingPort | O (= pol) | O | O | O | – | sea-port LOCODE; default = routing.pol (warning loading_port_defaulted_from_pol) | SO.MasterAddressFromId | GoodsConsignedPlace 565 + LoadingLocation 666 YES-V |
master.dischargeSite | O | O | O | O | O | import only (refused otherwise, not_applicable_for_trade); optional to create: the customs manifest waits for it (manifest_data). Given without routing.pod, it gives the port of discharge. A customs SiteLookup code in the tenant allow-list (sea: ILAST, ILASH, ILHFA, ILHBT, ILBXQ, ILMTS, ILCXQ, ILOVO; air: ILMMN, ILSWS), also a Freightools port record; must equal the carrier master's (581); no default for sea | SO.MasterAddressToId · air: no Freightools field seen | UnloadingLocation 810 YES-V (+ GoodsReceiptPlace 570 when equal) · A: FTD |
master.goodsReceiptSite | O (= dischargeSite) | O | O | O | O | import only. LCL: optional to create — the customs manifest waits for it (manifest_data, customs 1023) — and it should be a terminal: a port is accepted with warning lcl_receipt_site_must_be_terminal. FCL: defaults to dischargeSite (warning goods_receipt_site_defaulted) | Freightools field UNKNOWN → RECEIPT_SITE_FIELD_UNKNOWN (CR-5) | 570 is emitted by Freightools, not from your value today |
master.transactionNumber / master.manifestNumber | O / O | O / O | O / O | O / O | – | ^I[0-9A-Z]{9}$ / 6 digits; normally omit: Freightools fills them through its customs lookup. A value that conflicts with what Freightools already holds → override_conflicts_with_freightools | SO.CarrierTransactionNumber / SO.ManifestNumber (write acceptance FT-DEPENDENT) | IL2 / Declaration.ID YES-V |
house.document {number, type} | O / O | O / O | O / O | O / O | O / O | ≤ 35, trimmed; type → HBLType (UNVERIFIED encoding) | CS.HBL / CS.HBLType (left out: an update keeps Freightools' HBL) | NO — not emitted today although mandatory for forwarders → HBL_NOT_EMITTED (CR-1) · A: FTD 703 |
house.placeOfIssue | O | O | O | O | O | ≤ 256 | no Freightools field | NO → PLACE_OF_ISSUE_NOT_EMITTED · A: FTD 721 |
house.transactionNumber | O | O | O | O | – | ^I[0-9A-Z]{9}$; positions 2–4 must equal the master's (431, il1_prefix_mismatch); normally omit (Freightools' lookup allocates it) | CS.TransactionNumber | IL1 YES-V |
3.8 Commercial #
| Field | F | L | C | K | A | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|---|---|---|---|
commercial.incoterm | O | O | O | O | O | EXW FCA FAS FOB CFR CIF CPT CIP DAT DAP DPU DDP → tenant incoterm table (incoterm_unresolved; Admin → Companies → environment → Incoterm ids overrides the defaults); left out: derived, warning incoterm_derived (§3.2.1) | CS.IncotermId | freight payment code 560 YES-I (derived by Freightools from the incoterm) · A: FTD |
commercial.freightTerms | O | O | O | O | O | collect | prepaid; default E/F groups → collect, C/D → prepaid; a value that disagrees with the incoterm default → warning freight_terms_not_transmitted | CustomerShipmentGood.ShippingTerms (not proven to feed the XML) | NO independently of the incoterm → FREIGHT_TERMS_VIA_INCOTERM_ONLY (CR-16) |
commercial.freightCharge | – | – | – | – | O | {amount, currency, perKg?, rateTypeCode?} | CustomerShipmentGood.AWBRate (FT-DEPENDENT) | FTD 561–563 |
3.9 Cargo — FCL #
One entry per container. One CustomerShipmentGood is written per container with the ISO type always set explicitly (this prevents the Freightools fallback that produced customs error 4852 on a real shipment).
| Field | Req. | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|
containers[].number | O (booking) | ISO 6346 with check digit (invalid_container_number, customs 500); unique in the shipment (duplicate_container, 501) | Good.IdentificationNR | TE id 486 YES-V |
containers[].isoType | R | 4-character ISO 6346 size/type in the curated table and allowed for the FCL mix (invalid_iso_6346_type, freight_unit_not_in_mix) | Good.FreightUnitTypeId + Good.ISOEquipmentCode | type 483: fallback path VERIFIED, explicit override INFERRED → ISO_OVERRIDE_UNVERIFIED (CR-6). Agreement with the carrier master's type (4852) cannot be pre-checked (MASTER_CHECK_UNAVAILABLE) |
containers[].seal | O (booking) | ≤ 35; unique (duplicate_seal) | Good.SealNumber | seal 1002 YES-V (one seal) |
containers[].grossKg | O (an import's manifest needs it) | optional to create: Freightools takes a container without its weight, the customs manifest waits for it (manifest_data); > 0; Σ of the goods weights must equal it when goods carry weights (weight_mismatch) | Good.TotalWeight / UnitWeight | gross mass 430 YES-V |
containers[].packages {count, type, marks?} | O (booking); inside: R / R / O | count ≥ 1; type UNECE Rec 21 code (invalid_package_type); marks ≤ 200 | PackagesQuantity, KindOfPackage "<count><TYPE>", MarksAndNumbers | total packages 146 = container count, not your count → TOTAL_PACKAGES_IS_CONTAINER_COUNT; Packaging NO → PACKAGING_NOT_EMITTED (CR-2); marks NO |
containers[].cbm, tareKg, soc, reefer{active, temperatureC?} | O | > 0 / boolean | TotalVolume, TareWeight, (none), IsClimetControlled (UNVERIFIED link) | volume 203 YES-V; tare, SOC, reefer NO → OPTIONAL_STATEMENTS_NOT_EMITTED (CR-9) |
containers[].goods[] | R (1–50) | one HS per container in release 1 (multi_hs_per_container_unsupported) | see §3.12 | |
cargo.totalCbm | O | > 0 | CS total volume | — |
3.10 Cargo — LCL (variants L, C, K) #
LCL is modelled as package goods contained in a container good (VERIFIED on one Freightools LCL shipment). The shared container may come later: without cargo.container the customer shipment is written with its package goods alone, in one write (INFERRED), and when a later revision brings the container, the container good is added and the package goods already there are linked to it.
| Field | Req. | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|
cargo.container {number, isoType, seal?} | O (may come later; an import's manifest needs it); inside: O (booking) / R / O | as FCL; seal required when the tenant policy says so (seal_required, 510) | container good: FreightUnitTypeId, IsFULoad true, Quantity 1, number / seal / ISO; the number and seal are copied onto the package row | TE 31B YES-V |
cargo.packages[] | R | exactly one group in release 1 (lcl_multi_package_group_unsupported) | ||
packages[].count, type, grossKg | R / R / O (an import's manifest needs the weight) | type ∈ CT, BX, CS, PK until Freightools fixes its package-type codes (lcl_package_type_unsupported; pallets map to an unreliable code today); grossKg > 0, optional to create | package good: FreightUnitTypeId, Quantity = count, TotalWeight, ContainedInCSGoodsId → container good | total packages 146 = Σ Quantity YES-V; Packaging 93A YES-I → LCL_PACKAGE_TYPE_CODE_UNRELIABLE (CR-3) |
packages[].cbm, marks | O | TotalVolume, MarksAndNumbers | volume YES-V; marks NO | |
packages[].goods[] | R (1–50) | see §3.12 |
3.11 Cargo — Air #
| Field | Req. | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|
cargo.pieces[] | R (1–50) | one good per group | ||
pieces[].count, type, grossKg | R / R / O (an import's manifest needs the weight) | type must be a package unit allowed for the air mix (freight_unit_not_in_mix); grossKg > 0, optional to create; Σ kg must equal the MAWB gross for a single house (pieces_weight_mismatch, customs 454) | FreightUnitTypeId, Quantity, TotalWeight | FTD 205 / 430 / 93A |
pieces[].cbm, dimensionsCm{l,w,h}, chargeableKg | O | > 0 | TotalVolume, DimensionsL/W/H, ChargeableTotalWeight | FTD |
pieces[].goods[] | R | see §3.12 |
Air shipments are created and validated completely. Whether the customs send can follow depends on your Freightools air process template: the readiness check air_manifest_path is blocked while the air process steps carry no customs integration tool (CR-AIR) and ok once they do. This is a fact of your Freightools setup, not a limitation of this API.
3.12 Goods lines (all variants) #
| Field | Req. | Validation | Freightools receives | Customs receives |
|---|---|---|---|---|
goods[].hs | O (booking; an import's manifest needs it) | 6–10 digits (invalid_hs); HS6 → Freightools CommodityId (hs_unresolved). On the test environment Freightools' GetCommodities returns 404, so resolution uses a per-tenant table (warning hs_resolved_from_tenant_table; HS_RESOLUTION_BLOCKED_ON_TEST, CR-0) | Good.CommodityId; GoodsDescription "<hs> <description>" | HS 414 YES-V (Freightools emits 4 digits) |
goods[].description | R | ≤ 240 | GoodsDescription | 399 YES-V · A: FTD |
goods[].additionalHs[] | O | ≤ 10 codes | none | NO → ADDITIONAL_HS_NOT_EMITTED (CR-12) |
goods[].dangerous {unNumber, class} | C | required together (dg_fields_required_together); UN 4 digits (invalid_un_number, 480); class ^[1-9](\.[1-6])?$ (invalid_imdg_class) | IsDangerous, IMOLabel (the proven SSO source), IMOClass; UNNumber left empty → UN_NUMBER_FIELD_DUPLICATION (CR-15) | SSO + DG status D YES-V · A: FTD R |
goods[].dangerous.packingGroup | C | I | II | III; required for classes where customs demands it (packing_group_required, 611) | Freightools field UNKNOWN | 341 FT-DEPENDENT → OPTIONAL_STATEMENTS_NOT_EMITTED |
goods[].dangerous.properShippingName | O | ≤ 200 | none | NO |
goods[].vehicles[].vin | C | required when the package type is VN (vin_required, 1020); 17 characters (invalid_vin) | no Freightools field | NO → VIN_NOT_EMITTED (CR-10) |
goods[].value {amount, currency} | O | ISO 4217 → CurrencyId (invalid_currency, currency_unresolved) | Good.Value + CurrencyId | NO → VALUE_NOT_EMITTED |
3.13 Duplicate and ambiguous inputs — which one wins #
| Pair | Rule |
|---|---|
carrier.scac vs carrier.name / airline.iata vs icao vs MAWB prefix | the code is the key, the name is informational; disagreement → carrier_conflict / airline_conflict; the AWB prefix is always derived from the MAWB and must match → airline_awb_prefix_mismatch |
vessel.imo vs vessel.name | IMO is the key; a name that is not the ship's current or a former name → warning vessel_conflict (the IMO wins); a name alone with several matches → warning vessel_ambiguous (§3.6, Vessel) |
flight.number + flight.date vs eta | the flight date is the scheduled departure of the last leg before Israel (it builds the customs Declaration.ID), never the ETA |
partnerCode vs full party details | see §3.4 |
routing.pol vs master.loadingPort | house POL (CS leg) vs the carrier's loading port (SO master from): two destinations; loadingPort defaults to pol |
routing.pod vs master.dischargeSite vs master.goodsReceiptSite | commercial port (CS, LOCODE) vs customs unloading site (SO master to, must equal the carrier master) vs receipt terminal (LCL: the customs manifest needs it): three fields, three destinations; dischargeSite has no default for sea |
routing.placeOfReceipt vs routing.acceptancePlace | door pickup address vs the customs "place where the forwarder took charge" (free text) |
master.document vs house.document | master = the IL2 holder's document (line MBL, co-loader's HBL, MAWB); house = yours; the same number in both → master_equals_house |
shipmentRef vs parties.customer.reference | your key vs the paying customer's reference |
commercial.incoterm vs freightTerms | Freightools derives the payment code from the incoterm; an independent override cannot be transmitted → warning |
overrides (master.*Number, house.transactionNumber) vs Freightools values | an override that differs from what Freightools already holds → override_conflicts_with_freightools; customs keys are never overwritten silently |
4. Minimal examples (required fields only) #
"Required" here means required by the customs manifest, not required to create: the import examples 4.1–4.6 are small bodies whose manifest can be sent — each carries everything the readiness check manifest_data asks for, which is more than a shipment needs to be created. The smallest body that creates a shipment, and a revision that completes it, are in §3.1.1 (Start small).
All values below are fictional. The bodies are the files src/api/examples/*.json, reproduced verbatim — the same files /openapi.json carries as request examples and the tests validate against the schemas. Only public reference data is real: UN/LOCODEs, customs site codes, carrier SCACs and names, vessel names and IMO numbers, airline codes, HS codes, UNECE package codes and ISO 6346 size/type codes.
4.1 FCL, single container, dangerous goods #
{
"shipmentRef": "EXAMPLE-FCL-001",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Trading Co., Ltd",
"address": "12 Harbour Road",
"city": "Shenzhen",
"country": "CN"
},
"consignee": {
"name": "Example Imports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Tel Aviv-Yafo",
"country": "IL"
}
},
"dgContact": {
"company": "Example Forwarding Ltd",
"person": "Operations desk",
"phone": "+972500000000",
"phoneType": "TE"
},
"routing": {
"pol": "CNSZX"
},
"carriage": {
"carrier": {
"scac": "MSCU"
},
"vessel": {
"imo": "9308584",
"name": "MSC NINA F"
},
"voyage": "001E",
"eta": "2026-12-20"
},
"master": {
"document": {
"number": "MSCUEX0000001"
},
"loadingPort": "CNSHK",
"dischargeSite": "ILAST"
},
"house": {
"document": {
"number": "EXHBL0000001"
}
},
"commercial": {
"incoterm": "EXW"
},
"cargo": {
"containers": [
{
"number": "EXMU0000015",
"isoType": "45G1",
"seal": "SEAL000001",
"grossKg": 11500,
"packages": {
"count": 14,
"type": "PK"
},
"goods": [
{
"hs": "870310",
"description": "ELECTRIC VEHICLES",
"dangerous": {
"unNumber": "3556",
"class": "9"
}
}
]
}
]
}
}
routing.pod defaults to ILASH from ILAST (warning pod_defaulted_from_site); master.goodsReceiptSite defaults to the discharge site (warning goods_receipt_site_defaulted). dgContact is required because the goods line is dangerous and no tenant default DG contact is assumed.
4.2 FCL, seven 40'HC containers #
{
"shipmentRef": "EXAMPLE-FCL-007",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Panels S.A.",
"address": "8 Port Avenue",
"city": "Constanta",
"country": "RO"
},
"consignee": {
"name": "Example Imports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Tel Aviv-Yafo",
"country": "IL"
}
},
"routing": {
"pol": "ROCND"
},
"carriage": {
"carrier": {
"scac": "MSCU"
},
"vessel": {
"imo": "9376141",
"name": "MSC POLINA"
},
"voyage": "002W",
"eta": "2026-12-22"
},
"master": {
"document": {
"number": "MSCUEX0000007",
"type": "waybill"
},
"dischargeSite": "ILAST"
},
"house": {
"document": {
"number": "EXHBL0000007",
"type": "telex"
}
},
"commercial": {
"incoterm": "FCA"
},
"cargo": {
"containers": [
{
"number": "EXMU0001011",
"isoType": "45G0",
"seal": "SEAL000101",
"grossKg": 27000,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001027",
"isoType": "45G0",
"seal": "SEAL000102",
"grossKg": 27100,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001032",
"isoType": "45G0",
"seal": "SEAL000103",
"grossKg": 27200,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001048",
"isoType": "45G0",
"seal": "SEAL000104",
"grossKg": 27300,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001053",
"isoType": "45G0",
"seal": "SEAL000105",
"grossKg": 27400,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001069",
"isoType": "45G0",
"seal": "SEAL000106",
"grossKg": 27500,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
},
{
"number": "EXMU0001074",
"isoType": "45G0",
"seal": "SEAL000107",
"grossKg": 27600,
"packages": {
"count": 16,
"type": "PK"
},
"goods": [
{
"hs": "441011",
"description": "MELAMINE FACED CHIPBOARDS"
}
]
}
]
}
}
No dgContact (no dangerous goods). master.loadingPort defaults to routing.pol (warning loading_port_defaulted_from_pol); master.document.type (waybill) and house.document.type (telex) are optional.
4.3 FCL, three 20' containers with class 8 dangerous goods #
{
"shipmentRef": "EXAMPLE-FCL-DG",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Batteries A.S.",
"address": "3 Factory Road",
"city": "Izmit",
"country": "TR"
},
"consignee": {
"name": "Example Imports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Tel Aviv-Yafo",
"country": "IL"
}
},
"dgContact": {
"company": "Example Forwarding Ltd",
"person": "Operations desk",
"phone": "+972500000000",
"phoneType": "TE"
},
"routing": {
"pol": "TRIZT"
},
"carriage": {
"carrier": {
"scac": "YMLU"
},
"vessel": {
"imo": "9757230",
"name": "YM WELLSPRING"
},
"voyage": "003E",
"eta": "2026-12-18"
},
"master": {
"document": {
"number": "YMLUEX0000003"
},
"dischargeSite": "ILASH"
},
"house": {
"document": {
"number": "EXHBL0000003"
}
},
"commercial": {
"incoterm": "FOB"
},
"cargo": {
"containers": [
{
"number": "EXMU0002018",
"isoType": "22G0",
"seal": "SEAL000201",
"grossKg": 25400,
"packages": {
"count": 28,
"type": "PK"
},
"goods": [
{
"hs": "850710",
"description": "BATTERIES, WET, FILLED WITH ACID, ELECTRIC STORAGE",
"dangerous": {
"unNumber": "2794",
"class": "8"
}
}
]
},
{
"number": "EXMU0002023",
"isoType": "22G0",
"seal": "SEAL000202",
"grossKg": 25350,
"packages": {
"count": 28,
"type": "PK"
},
"goods": [
{
"hs": "850710",
"description": "BATTERIES, WET, FILLED WITH ACID, ELECTRIC STORAGE",
"dangerous": {
"unNumber": "2794",
"class": "8"
}
}
]
},
{
"number": "EXMU0002039",
"isoType": "22G0",
"seal": "SEAL000203",
"grossKg": 25300,
"packages": {
"count": 28,
"type": "PK"
},
"goods": [
{
"hs": "850710",
"description": "BATTERIES, WET, FILLED WITH ACID, ELECTRIC STORAGE",
"dangerous": {
"unNumber": "2794",
"class": "8"
}
}
]
}
]
}
}
One dgContact covers every dangerous goods line of the shipment. Each container carries exactly one goods line (release 1: multi_hs_per_container_unsupported).
4.4 LCL under a line master #
{
"shipmentRef": "EXAMPLE-LCL-001",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "LCL",
"lcl": {
"arrangement": "lineMaster"
}
},
"parties": {
"shipper": {
"name": "Example Textiles SAS",
"address": "21 Quai Example",
"city": "Le Havre",
"country": "FR"
},
"consignee": {
"name": "Example Retail Ltd",
"taxId": "510000342",
"address": "9 Market Street",
"city": "Netanya",
"country": "IL"
}
},
"routing": {
"pol": "FRLEH"
},
"carriage": {
"carrier": {
"name": "MEDKON"
},
"vessel": {
"imo": "9366457",
"name": "MEDKON PERLA"
},
"voyage": "004E",
"eta": "2026-12-15"
},
"master": {
"document": {
"number": "MEDKEX0000001"
},
"dischargeSite": "ILASH",
"goodsReceiptSite": "ILBXQ"
},
"house": {
"document": {
"number": "EXHBL0000011",
"type": "telex"
}
},
"commercial": {
"incoterm": "FOB"
},
"cargo": {
"container": {
"number": "EXMU0003014",
"isoType": "45G1",
"seal": "SEAL000301"
},
"packages": [
{
"count": 4,
"type": "CT",
"grossKg": 842.5,
"goods": [
{
"hs": "620342",
"description": "MEN'S COTTON TROUSERS"
}
]
}
]
}
}
LCL package types are limited to CT, BX, CS, PK in release 1 (lcl_package_type_unsupported, CR-3): pallets (PX) are rejected until Freightools fixes its package-type codes. An LCL import's customs manifest needs master.goodsReceiptSite (a shipment created without it waits on manifest_data), and it should be a terminal: a port is accepted with warning lcl_receipt_site_must_be_terminal. The carrier is given by carrier.name here; a name is accepted when it resolves to exactly one Freightools carrier (carrier_ambiguous otherwise).
4.5 LCL co-load under another forwarder's house #
{
"shipmentRef": "EXAMPLE-LCL-COLOAD",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "LCL",
"lcl": {
"arrangement": "coload"
}
},
"parties": {
"shipper": {
"name": "Example Chemie GmbH",
"address": "Hafenstrasse 1",
"city": "Hamburg",
"country": "DE"
},
"consignee": {
"name": "Example Chemicals Ltd",
"taxId": "510000565",
"address": "14 Science Park",
"city": "Haifa",
"country": "IL"
},
"coloader": {
"name": "Example Consolidation GmbH",
"address": "Speicherstadt 2",
"city": "Hamburg",
"country": "DE"
}
},
"dgContact": {
"company": "Example Forwarding Ltd",
"person": "Operations desk",
"phone": "+972500000000",
"phoneType": "TE"
},
"routing": {
"pol": "DEHAM"
},
"carriage": {
"eta": "2026-12-12"
},
"master": {
"issuer": "coloader",
"document": {
"number": "EXCONSOL000001"
},
"dischargeSite": "ILASH",
"goodsReceiptSite": "ILMTS"
},
"house": {
"document": {
"number": "EXHBL0000021"
}
},
"commercial": {
"incoterm": "CIF"
},
"cargo": {
"container": {
"number": "EXMU0004010",
"isoType": "45G1"
},
"packages": [
{
"count": 2,
"type": "CT",
"grossKg": 1610.4,
"cbm": 2.75,
"marks": "EXAMPLE CHEMICALS 1-2",
"goods": [
{
"hs": "330210",
"description": "ODORIFEROUS MIXTURES",
"dangerous": {
"unNumber": "3082",
"class": "9",
"packingGroup": "III"
}
}
]
}
]
}
}
The master is the co-loader's house B/L (master.issuer: "coloader", parties.coloader required), so no carriage.carrier is given (the carrier, the vessel and the voyage are optional in every variant). Expected at customs: IL2 in house format, condition code 30.
4.6 Air import house #
{
"shipmentRef": "EXAMPLE-AIR-001",
"submitManifest": true,
"service": {
"mode": "air"
},
"parties": {
"shipper": {
"name": "Example Garments Co., Ltd",
"address": "88 Textile Avenue",
"city": "Shanghai",
"country": "CN"
},
"consignee": {
"name": "Example Fashion Ltd",
"taxId": "510000789",
"address": "15 Kinneret Street",
"city": "Bnei Brak",
"country": "IL"
}
},
"routing": {
"aol": "CNPVG",
"aod": "ILTLV",
"acceptancePlace": "SHANGHAI"
},
"carriage": {
"airline": {
"iata": "EY"
},
"flight": {
"number": "123",
"date": "2026-12-10"
}
},
"master": {
"document": {
"number": "607-12345675"
},
"dischargeSite": "ILMMN"
},
"house": {
"document": {
"number": "EXHAWB000001"
},
"placeOfIssue": "SHANGHAI"
},
"commercial": {
"incoterm": "FOB"
},
"cargo": {
"pieces": [
{
"count": 33,
"type": "CT",
"grossKg": 285,
"goods": [
{
"hs": "611595",
"description": "KNITTED GARMENTS"
}
]
}
]
}
}
Creates the CS/SO; the send stays blocked (readiness check air_manifest_path) while the Freightools air process steps carry no customs integration tool (CR-AIR). flight.date is the scheduled departure of the last leg before Israel, not the ETA.
4.7 Everything optional (FCL base of 4.1 with every optional member) #
This is also the OpenAPI example fcl-everything-optional (the file src/api/examples/fcl-everything-optional.json, reproduced here verbatim). Every optional value customs will not receive comes back in knownDeviations[]; options.dryRun: true runs the full validation and resolution without writing anything.
{
"shipmentRef": "EXAMPLE-FCL-001",
"submitManifest": true,
"service": {
"mode": "ocean",
"load": "FCL",
"scope": "door-port"
},
"parties": {
"shipper": {
"name": "Example Trading Co., Ltd",
"address": "12 Harbour Road",
"city": "Shenzhen",
"country": "CN",
"zip": "518000",
"phone": "+8675500000000",
"email": "export@example-trading.test",
"partnerCode": "EXT-001",
"taxId": "IDCN914403000001",
"taxIdType": "company"
},
"consignee": {
"name": "Example Imports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Tel Aviv-Yafo",
"country": "IL",
"zip": "6701101",
"phone": "+97230000000",
"email": "imports@example-imports.test",
"partnerCode": "EXI-001",
"taxIdType": "company"
},
"notify": {
"name": "Example Logistics Ltd",
"address": "2 Port Road",
"city": "Ashdod",
"country": "IL",
"phone": "+97280000000"
},
"customer": {
"party": "consignee",
"reference": "PO-2026-000123"
},
"customsBroker": {
"name": "Example Customs Brokers Ltd",
"taxId": "510000904",
"address": "7 Customs Square",
"city": "Ashdod",
"country": "IL"
}
},
"dgContact": {
"company": "Example Forwarding Ltd",
"person": "Operations desk",
"phone": "+972500000000",
"phoneType": "TE"
},
"routing": {
"pol": "CNSZX",
"pod": "ILASH",
"placeOfReceipt": {
"country": "CN",
"city": "Dongguan",
"zip": "523000",
"address": "1 Example Factory Road"
},
"acceptancePlace": "DONGGUAN",
"transshipmentPorts": [
"SGSIN"
]
},
"carriage": {
"carrier": {
"scac": "MSCU",
"name": "MSC"
},
"vessel": {
"imo": "9308584",
"name": "MSC NINA F"
},
"voyage": "001E",
"eta": "2026-12-20",
"etd": "2026-11-15",
"booking": "EXBOOK000001",
"transshipments": [
{
"port": "SGSIN",
"vessel": {
"imo": "9376141",
"name": "MSC POLINA"
},
"voyage": "002W",
"eta": "2026-12-05"
}
]
},
"master": {
"issuer": "carrier",
"document": {
"number": "MSCUEX0000001",
"type": "original"
},
"loadingPort": "CNSHK",
"dischargeSite": "ILAST",
"goodsReceiptSite": "ILAST"
},
"house": {
"document": {
"number": "EXHBL0000001",
"type": "original"
},
"placeOfIssue": "SHENZHEN"
},
"commercial": {
"incoterm": "EXW",
"freightTerms": "collect"
},
"cargo": {
"containers": [
{
"number": "EXMU0000015",
"isoType": "45G1",
"seal": "SEAL000001",
"grossKg": 11500,
"cbm": 68,
"tareKg": 3900,
"soc": false,
"reefer": {
"active": false
},
"packages": {
"count": 14,
"type": "PK",
"marks": "EXAMPLE IMPORTS 1-14"
},
"goods": [
{
"hs": "870310",
"description": "ELECTRIC VEHICLES",
"additionalHs": [
"870380"
],
"dangerous": {
"unNumber": "3556",
"class": "9",
"packingGroup": "III",
"properShippingName": "VEHICLE, LITHIUM ION BATTERY POWERED"
},
"vehicles": [
{
"vin": "1HGBH41JXMN109186"
}
],
"value": {
"amount": 42000,
"currency": "USD"
}
}
]
}
],
"totalCbm": 68
},
"options": {
"dryRun": true
}
}
The overrides master.transactionNumber, master.manifestNumber and house.transactionNumber are deliberately absent: in normal use leave them out — Freightools fills them through its customs lookup and the shipment waits for them (§9).
4.8 FCL export from Israel at booking (no numbers yet, incoterm derived) #
The Israeli exporter is the shipper (here with its tax id, which is optional); no Israeli customs fields; two 40' containers by type and weight only (the weight could be left out too). The trade is worked out from the ends (Haifa → Hamburg = export); "service": { …, "trade": "export" } would say the same. The incoterm is left out: the customer is the exporter (no party given on an export) and there is no delivery door, so it is derived as CFR (warning incoterm_derived). Container numbers, seals and the B/L numbers follow in a revision.
{
"shipmentRef": "EXAMPLE-EXP-001",
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Exports Ltd",
"taxId": "510000128",
"address": "5 Industrial Street",
"city": "Haifa",
"country": "IL"
},
"consignee": {
"name": "Example Handel GmbH",
"address": "1 Hafenstrasse",
"city": "Hamburg",
"country": "DE"
},
"customer": {
"reference": "PO-EXAMPLE-77"
}
},
"routing": {
"pol": "ILHFA",
"pod": "DEHAM"
},
"carriage": {
"carrier": {
"scac": "MSCU"
},
"etd": "2026-12-02",
"eta": "2026-12-20",
"booking": "EXBOOK0000001"
},
"cargo": {
"containers": [
{
"isoType": "45G1",
"grossKg": 12000,
"goods": [
{
"description": "MACHINE PARTS"
}
]
},
{
"isoType": "45G1",
"grossKg": 11500,
"goods": [
{
"description": "MACHINE PARTS"
}
]
}
]
}
}
4.9 FCL cross-trade, tracking off #
Neither end is in Israel, so no party has to be Israeli and there are no customs steps. tracking.active: false keeps Freightools tracking off for this shipment whatever the company default.
{
"shipmentRef": "EXAMPLE-XT-001",
"service": {
"mode": "ocean",
"load": "FCL"
},
"parties": {
"shipper": {
"name": "Example Trading Co., Ltd",
"address": "12 Harbour Road",
"city": "Shenzhen",
"country": "CN"
},
"consignee": {
"name": "Example Handel GmbH",
"address": "1 Hafenstrasse",
"city": "Hamburg",
"country": "DE"
},
"customer": {
"party": "consignee",
"reference": "PO-EXAMPLE-88"
}
},
"routing": {
"pol": "CNSZX",
"pod": "DEHAM"
},
"carriage": {
"carrier": {
"scac": "MSCU"
},
"eta": "2027-01-15"
},
"master": {
"document": {
"number": "MSCUEX0000205"
}
},
"house": {
"document": {
"number": "EXHBL0000205",
"type": "waybill"
}
},
"commercial": {
"incoterm": "FOB"
},
"cargo": {
"containers": [
{
"number": "EXMU0000205",
"isoType": "45G1",
"seal": "SEAL000205",
"grossKg": 9800,
"packages": {
"count": 40,
"type": "CT"
},
"goods": [
{
"hs": "441011",
"description": "PARTICLE BOARD"
}
]
}
]
},
"tracking": {
"active": false
}
}
5. Validation layers #
Validation runs completely before any Freightools write, stops at the first failing layer and returns every finding of that layer, so you fix one layer at a time with no partial side effects.
| Layer | HTTP | type | What it checks |
|---|---|---|---|
| L0 schema | 400 | validation_error | shape, required fields, unknown fields, types, lengths, variant applicability |
| L1 codes and formats | 400 | validation_error | ISO/UN codes, check digits, regexes, dates, placeholders |
| L2 reference resolution | 422 | resolution_error | every code resolves to exactly one Freightools record in your environment |
| L3 supported combination | 422 | unsupported | what release 1 can express in Freightools |
| L4 cross-field | 422 | consistency_error | sums, pairs, customs checks that can be done before the send |
| L5 tenant policy | 422 | policy_error | your tenant's settings and gates |
6. Error envelope, codes, warnings and known deviations #
6.1 Envelope #
Every non-2xx response is JSON:
{ "type": "consistency_error", "status": 422, "requestId": "req_01JC…", "shipmentRef": "EXAMPLE-FCL-001",
"errors": [ { "path": "/cargo/containers/0/grossKg", "code": "weight_mismatch",
"message": "Sum of goods weights (11200 kg) differs from the container gross weight (11500 kg)",
"value": 11500, "hint": "Set grossKg to the sum of the goods lines, or correct the goods weights" } ],
"warnings": [] }
path is a JSON Pointer into your body ('' = whole body). The same {path, code, message, value?, hint?} shape is used for errors[], warnings[] and knownDeviations[], in the API and in the portal. Quote requestId when you contact support.
Other statuses you will meet, same envelope: 401 (no/invalid token), 403 (scope or IP), 404 (unknown id, or the shipment belongs to another environment), 409 (transmission_exists, not_ready, decision_stale, illegal_transition), 412 (precondition_failed: If-Match mismatch), 413 (payload_too_large: body > 1 MiB), 429 (rate_limited, §13), 501 (preview_not_verified, not_implemented), 503 (gate_closed on the manual send/preview; tenant_not_configured when the environment has no license key).
6.2 Error codes by layer #
L0 — schema (400 validation_error)
| Code | Meaning |
|---|---|
required | a required field is missing (few are: §3.1.1). An ocean shipment's missing port of discharge is reported with the same code by L4, as a 422 |
unknown_field | a field the API does not know (typo protection) |
type_mismatch | wrong JSON type (e.g. a string where a number is expected) |
too_long / too_short | string or array outside its length limits |
invalid_enum | value not in the allowed set |
invalid_format | value does not match its pattern (date, code shape) |
not_applicable_for_variant | block or field that does not exist for this mode/load |
empty_array | an array that needs at least one element is empty |
invalid_json | the body is not valid JSON |
L1 — codes and formats (400 validation_error)
| Code | Meaning |
|---|---|
invalid_country | not an ISO 3166-1 alpha-2 code |
invalid_unlocode | not a 5-character UN/LOCODE |
invalid_container_number | ISO 6346 check digit fails (customs 500) |
invalid_iso_6346_type | size/type code not in the curated ISO 6346 table |
invalid_package_type | not a UNECE Rec 21 package code |
invalid_hs | HS code is not 6–10 digits |
invalid_un_number | UN number is not 4 digits (customs 480) |
invalid_imdg_class | IMDG class is not 1–9 with an optional .1–.6 division |
invalid_mawb | MAWB is not NNN-NNNNNNNN |
invalid_tax_id | consignee id is not 9 digits or a valid passport form (customs 553) |
invalid_phone | phone has fewer than 6 digits or more than 50 characters (customs 1013) |
placeholder_address | address/city/name is a placeholder such as N/A |
duplicate_container | the same container number appears twice (customs 501) |
eta_before_etd | ETA earlier than ETD |
eta_before_departure | ETA earlier than the flight / transshipment departure |
invalid_date | not YYYY-MM-DD or not a real date |
invalid_transaction_number | customs transaction number not I + 9 characters |
invalid_currency | not ISO 4217 |
invalid_site_code | customs site code malformed |
negative_or_zero | a quantity, weight or amount that must be positive is not |
The codes invalid_scac, invalid_airline_code, invalid_incoterm, invalid_flight_number, invalid_manifest_number, invalid_vin, invalid_shipment_ref and invalid_email exist in src/domain/codes.ts but nothing emits them: those shapes are enforced by the JSON schema at L0, so a bad SCAC, flight number, manifest number, VIN, shipmentRef (^[A-Za-z0-9._/-]{1,60}$) or e-mail comes back as invalid_format, and a bad incoterm as invalid_enum, each with the field's JSON Pointer.
L2 — reference resolution (422 resolution_error)
| Code | Meaning |
|---|---|
port_unresolved / airport_unresolved | LOCODE is not a sea port / airport in your Freightools environment |
site_not_allowed | customs site not in the sea/air allow-list |
carrier_unresolved / carrier_ambiguous / carrier_conflict | SCAC or name not found / several matches / SCAC and name disagree |
airline_unresolved / airline_conflict / airline_awb_prefix_mismatch | airline not in the table / IATA and ICAO disagree / MAWB prefix is another airline's |
incoterm_unresolved | the tenant incoterm table has no id for this code |
freight_unit_unmapped / freight_unit_not_in_mix | ISO type or package type has no Freightools freight unit / the unit is not allowed for this service mix |
hs_unresolved | no CommodityId for this HS (live lookup and tenant table) |
partner_ambiguous | several partners match the party |
party_code_tax_id_mismatch | partnerCode resolves to a partner with another taxId or name |
country_unresolved / currency_unresolved | code valid but unknown to Freightools |
reference_unavailable | Freightools reference data could not be read (retry later) |
L3 — supported combination (422 unsupported)
| Code | Meaning |
|---|---|
multi_hs_per_container_unsupported | more than one goods line per FCL container (release 1) |
lcl_package_type_unsupported | LCL package type outside CT BX CS PK (CR-3) |
lcl_multi_package_group_unsupported | more than one LCL package group (release 1) |
consolidation_unsupported | variant K until evidence set B1 |
transit_procedure_unsupported / export_unsupported | out of scope |
unsupported_change | a same-ref update that changes the variant; use a new shipmentRef |
goods_remove_unverified / lcl_container_relink_unverified | a same-ref update that removes a goods line / moves a package to another container — routed to an operator until Freightools confirms the semantics |
L4 — cross-field (422 consistency_error)
| Code | Meaning |
|---|---|
weight_mismatch | Σ goods weights ≠ container gross |
master_equals_house | master and house document numbers are the same |
issuer_arrangement_mismatch | master.issuer disagrees with service.lcl.arrangement |
coloader_required | co-load without parties.coloader |
consignee_is_submitter | consignee id equals your customs submitter id (customs 412) |
consignee_country_not_il | an import's consignee country is not IL |
shipper_country_not_il | an export's shipper country is not IL (the Israeli exporter) |
customer_required | the shipment's customer is not in the body: neither the consignee nor the shipper is given (and no parties.customer.details), or parties.customer.party names a party that is missing (§3.2.1); reported at /parties/<party>/name or /parties/customer/details/name |
required | an ocean shipment without routing.pod — an import may give master.dischargeSite instead; reported at /routing/pod |
etd_required_with_booking | an ocean carriage.booking without carriage.etd |
trade_conflict | service.trade disagrees with the ends of the route (§3.2.1) |
trade_domestic | both ends of the main carriage are in Israel |
not_applicable_for_trade | an Israeli customs field (discharge / receipt site, transaction or manifest number) on an export or cross-trade |
manifest_not_applicable | submitManifest: true on an export or cross-trade |
dg_contact_required | dangerous goods without a DG contact and no tenant default (customs 602) |
dg_fields_required_together | unNumber without class or vice versa |
packing_group_required | packing group missing where customs demands it (611) |
vin_required | package type VN without VINs (1020) |
seal_required | seal missing where the tenant policy requires one (510) |
lcl_receipt_site_required | no longer raised: an LCL import is created without its goods-receipt site and its customs manifest waits for it (readiness check manifest_data, §9.2) |
lcl_receipt_site_must_be_terminal | a warning, never an error: an LCL import's goods-receipt site is a port, not a terminal (1023) |
door_address_required | door scope without the door address |
flight_airline_mismatch | flight designator belongs to another airline |
il1_prefix_mismatch | house transaction prefix ≠ master's (431) |
override_conflicts_with_freightools | an override differs from what Freightools already holds |
freight_terms_disagree | freight terms contradict the incoterm where the tenant policy makes that an error |
pieces_weight_mismatch | Σ air pieces ≠ MAWB gross (454) |
duplicate_seal | the same seal on two containers |
packages_count_mismatch | package counts disagree between levels |
L5 — tenant policy (422 policy_error)
| Code | Meaning |
|---|---|
partner_update_blocked | the change would rewrite a partner the tenant policy protects |
vessel_creation_blocked | unknown vessel and the tenant does not allow creating vessels |
environment_mismatch | the key's environment cannot serve this request (decided by the auth layer as 403 before the body is read) |
site_not_in_tenant_allowlist | the site is valid but your tenant has not enabled it |
writes_disabled | reserved; today a closed write gate is not a validation error — the shipment is recorded and processing.state becomes blocked (§2.1) |
tenant_not_configured | the environment has no Freightools license key — answered as 503 tenant_not_configured before validation |
6.3 Warnings #
Warnings never block; they tell you what was decided for you or what customs will not see.
| Code | Meaning |
|---|---|
party_details_differ_from_partner | the matched Freightools partner has another address/phone; customs receives the partner's |
partner_data_diverges | a shared partner was not rewritten |
partner_tel_placeholder | Freightools requires a phone; "-" was written |
partner_address_placeholder | Freightools requires an address and a city on a partner; the party came without one of them and its Freightools partner holds none, so "-" was written until a revision brings the real ones (an import's customs manifest needs them). Nothing a Freightools partner already holds (its tax id, address, city, phone, postcode, e-mail) is ever overwritten by an empty value: a later shipment may name the same partner by its name and country alone |
carrier_partner_created | the company had no Freightools partner for the carrier, so one was created (a supplier holding the system carrier and the carrier role, address fields "-") and set on the shipping order |
carrier_role_added | a carrier partner created by an earlier shipment was not yet marked as a carrier in Freightools; it is now |
carrier_partner_missing | no carrier partner could be found or created; the shipping order went without its carrier |
freight_terms_not_transmitted | your freightTerms disagree with the incoterm default and cannot reach customs independently |
pod_defaulted_from_site / loading_port_defaulted_from_pol / goods_receipt_site_defaulted / acceptance_place_defaulted | a default was applied; the chosen value is in value |
incoterm_derived | no commercial.incoterm was sent; the derived one is in value and the message says why (§3.2.1) |
optional_value_not_transmitted | an optional value was stored but has no customs path |
hs_resolved_from_tenant_table | HS resolved from the tenant table, not from Freightools |
delivery_order_not_received | the carrier has not issued the delivery order yet (customs 3204 is a warning on acceptance) |
vessel_imo_invalid / vessel_unknown / vessel_ambiguous / vessel_conflict / vessel_matched_by_name | the vessel: a bad IMO check digit / not found / several matches / the name is not the IMO's / matched by its name — never an error (§3.6, Vessel) |
6.4 Known deviations #
knownDeviations[] lists, per shipment, what customs does not receive today and the Freightools change request that would fix it. They are facts about Freightools' manifest generation, surfaced so nobody mistakes "stored" for "transmitted".
| Code | CR | What it means for you |
|---|---|---|
HBL_NOT_EMITTED | CR-1 | your house B/L number is not in the manifest |
PLACE_OF_ISSUE_NOT_EMITTED | CR-1 | house.placeOfIssue has no path |
DG_CONTACT_NOT_EMITTED | CR-1b | the DG contact is kept by us only |
TOTAL_PACKAGES_IS_CONTAINER_COUNT / PACKAGING_NOT_EMITTED | CR-2 | FCL: customs sees the container count as total packages; package type not sent |
LCL_PACKAGE_TYPE_CODE_UNRELIABLE | CR-3 | LCL package code derived from an unreliable Freightools column |
PARENT_SERIAL_NOT_EMITTED | CR-4 | parent serial for subset-of-master cases |
RECEIPT_SITE_FIELD_UNKNOWN | CR-5 | goods-receipt site emitted from an unidentified field |
ISO_OVERRIDE_UNVERIFIED | CR-6 | explicit ISO type precedence not yet witnessed |
CONSIGNOR_ID_FIELD_UNKNOWN | CR-8 | shipper tax id source unknown |
OPTIONAL_STATEMENTS_NOT_EMITTED | CR-9 | tare / SOC / reefer / packing group |
NOTIFY_NOT_EMITTED / VALUE_NOT_EMITTED | CR-9 | notify party / goods value |
VIN_NOT_EMITTED | CR-10 | VINs |
ACCEPTANCE_PLACE_IS_COUNTRY_CODE | CR-11 | the manifest carries the POL country, not your text |
ADDITIONAL_HS_NOT_EMITTED | CR-12 | extra HS codes |
COLOAD_MASTER_FIELDS_UNVERIFIED / CONSOLIDATION_MODEL_UNVERIFIED | CR-13 / CR-14 | co-load / consolidation Freightools model |
UN_NUMBER_FIELD_DUPLICATION | CR-15 | UN number written once (IMOLabel) to avoid a duplicate statement |
FREIGHT_TERMS_VIA_INCOTERM_ONLY | CR-16 | freight terms reach customs only through the incoterm |
HS_RESOLUTION_BLOCKED_ON_TEST | CR-0 | test environment resolves HS from a tenant table |
MASTER_CHECK_UNAVAILABLE | CR-18 | agreement with the carrier master (type, serial, site, DG) cannot be pre-checked; customs checks it (4852 etc.) |
7. The shipment representation #
Returned by POST /v1/shipments, GET /v1/shipments/{id} and GET /v1/shipments/by-ref/{ref}. Since release 2 it also carries deliveryOrder: null, or the delivery-order details collected in the portal (details, a completeness list of checks with ok | missing | invalid | waiting | blocked and ready, updatedAt, updatedBy) — delivery orders are not transmitted to customs yet (phase 3). The portal renders exactly this object (both are built by buildShipmentView in src/domain/read-models.ts). The example below is the updated_not_retransmitted scenario that /openapi.json carries, produced by the serialiser itself (src/api/examples.ts → buildShipmentView), so it cannot drift from what the server returns.
{
"id": "0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"shipmentRef": "EXAMPLE-FCL-001",
"version": 7,
"etag": "\"7\"",
"service": {
"mode": "ocean",
"load": "FCL",
"lclArrangement": null,
"variant": "F"
},
"revisions": {
"current": {
"id": "11111111-2222-4333-8444-555555555552",
"rev": 2,
"kind": "data",
"state": "applied",
"dataHash": "a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6",
"submitManifest": true,
"receivedAt": "2026-10-25T09:00:00.000Z",
"appliedAt": "2026-10-25T09:04:00.000Z",
"changedPaths": [
"/cargo/containers/0/grossKg"
],
"submittedBy": {
"type": "api_key",
"id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a"
}
},
"lastApplied": {
"id": "11111111-2222-4333-8444-555555555552",
"rev": 2,
"kind": "data",
"state": "applied",
"dataHash": "a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6",
"submitManifest": true,
"receivedAt": "2026-10-25T09:00:00.000Z",
"appliedAt": "2026-10-25T09:04:00.000Z",
"changedPaths": [
"/cargo/containers/0/grossKg"
],
"submittedBy": {
"type": "api_key",
"id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a"
}
},
"lastTransmitted": {
"id": "11111111-2222-4333-8444-555555555551",
"rev": 1,
"kind": "data",
"state": "applied",
"dataHash": "3f1a0c6b9d2e8f7a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a",
"submitManifest": true,
"receivedAt": "2026-10-25T07:00:00.000Z",
"appliedAt": "2026-10-25T07:04:00.000Z",
"changedPaths": [],
"submittedBy": {
"type": "api_key",
"id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a"
}
},
"inSync": false,
"count": 2
},
"processing": {
"state": "ready",
"readiness": {
"checks": [
{
"id": "manifest_number",
"status": "waiting",
"detail": "SO.ManifestNumber not filled by the Freightools customs lookup yet",
"nextCheckAt": "2026-10-25T13:00:00.000Z"
},
{
"id": "master_transaction",
"status": "waiting",
"detail": "SO.CarrierTransactionNumber (IL2) not filled yet"
},
{
"id": "house_transaction",
"status": "waiting",
"detail": "CS.TransactionNumber (IL1) not filled yet"
},
{
"id": "delivery_order",
"status": "waiting",
"detail": "SO.DeliveryOrderReceived is false (policy warn)"
},
{
"id": "il1_unique_on_manifest",
"status": "ok",
"detail": "no live transmission uses this (manifest, IL1) pair"
},
{
"id": "execution_step",
"status": "ok",
"detail": "execution step 800001 covers the container goods"
},
{
"id": "iso_type_vs_master",
"status": "unknown",
"detail": "no public path to the carrier master (CR-18)"
},
{
"id": "master_serials",
"status": "unknown",
"detail": "no public path to the carrier master (CR-18)"
},
{
"id": "unloading_site_vs_master",
"status": "unknown",
"detail": "no public path to the carrier master (CR-18)"
},
{
"id": "dg_flag_vs_master",
"status": "unknown",
"detail": "no public path to the carrier master (CR-18)"
},
{
"id": "send_gates",
"status": "ok",
"detail": "customs send gates open"
}
],
"masterChecks": "not_available",
"evaluatedAt": "2026-10-25T07:05:00.000Z"
}
},
"manifest": {
"state": "updated_not_retransmitted",
"lastOutcome": "accepted",
"autoSend": "disarmed",
"submitManifest": true
},
"customs": {
"manifestNumber": "123456",
"houseTransactionNumber": "I000111A01",
"masterTransactionNumber": "I000111A00",
"consignmentSequence": 6501,
"deliveryOrderReceivedAt": null
},
"freightools": {
"environment": "test",
"customerShipment": {
"id": "400001",
"code": "CS 030001"
},
"shippingOrder": {
"id": "250001",
"code": "SO 030001-2026 - O-I - FCL/FCL"
},
"manifestExecutionStepId": "800001"
},
"diff": [
{
"path": "/cargo/containers/0/grossKg",
"from": 11500,
"to": 11800
}
],
"actionsRequired": [
{
"id": "33333333-4444-4555-8666-777777777771",
"type": "customs_decision",
"state": "open",
"revision": 2,
"transmissionId": "22222222-3333-4444-8555-666666666661",
"details": {},
"options": [
"keep",
"correction",
"cancel_and_resend",
"cancel_only",
"refer_to_operator"
],
"createdAt": "2026-10-25T09:04:00.000Z",
"closedAt": null
}
],
"transmissions": [
{
"id": "22222222-3333-4444-8555-666666666661",
"kind": "initial",
"state": "feedback_received",
"revision": 1,
"manifestNumber": "123456",
"il1": "I000111A01",
"consignmentSequence": 6501,
"ftLogId": "9912001",
"applicationId": "APP-77120",
"sentAt": "2026-10-25T07:07:00.000Z",
"feedbackAt": "2026-10-25T07:08:00.000Z",
"outcome": {
"statusCode": "1",
"statusNameHe": "נקלט",
"outcome": "accepted",
"manifestState": "accepted_with_exceptions",
"functionCode": "90",
"manifestNumber": "123456",
"il1": "I000111A01",
"applicationId": "APP-77120",
"errors": [
{
"code": "4852",
"messageHe": "סוג מכולה אינו תואם למצהר הקו",
"messageEn": "Container type differs from the carrier master",
"severity": "warning",
"path": "/cargo/containers/0/isoType",
"hint": "Set isoType to the type on the carrier master (the master cannot be pre-checked, CR-18)",
"itemSerial": "1"
}
],
"requiredDocuments": []
},
"confirmationId": null,
"error": null
}
],
"decisions": [],
"documents": {
"count": 0,
"items": [],
"levelsNotice": "Uploading a document stores it here only. Customs transmission of documents is not available yet."
},
"knownDeviations": [
{
"path": "/house/document/number",
"code": "HBL_NOT_EMITTED",
"message": "The house B/L number is never emitted in the manifest (Freightools change request CR-1)",
"variant": "F",
"hint": "CR-1"
},
{
"path": "/dgContact",
"code": "DG_CONTACT_NOT_EMITTED",
"message": "The DG contact (UNDGContact 37B, customs check 602) has no Freightools field and is not transmitted (Freightools change request CR-1b)",
"variant": "F",
"hint": "CR-1b"
},
{
"path": "/cargo/containers/0/packages",
"code": "TOTAL_PACKAGES_IS_CONTAINER_COUNT",
"message": "For FCL Freightools sends the container count as total packages (146), not the package count (Freightools change request CR-2)",
"variant": "F",
"hint": "CR-2"
}
],
"warnings": [
{
"path": "/routing/pod",
"code": "pod_defaulted_from_site",
"message": "/routing/pod defaulted to \"ILASH\" from /master/dischargeSite",
"value": "ILASH"
},
{
"path": "/master/goodsReceiptSite",
"code": "goods_receipt_site_defaulted",
"message": "/master/goodsReceiptSite defaulted to \"ILAST\" from /master/dischargeSite",
"value": "ILAST"
}
],
"timestamps": {
"createdAt": "2026-10-25T07:00:00.000Z",
"updatedAt": "2026-10-25T09:04:00.000Z"
},
"links": {
"self": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"revisions": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/revisions",
"transmissions": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/transmissions",
"decisions": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/manifest/decisions",
"actions": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/actions",
"documents": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/documents",
"feedback": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/feedback",
"audit": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/audit",
"diff": "/v1/shipments/0f6c1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b/diff?from=transmitted&to=current"
}
}
Freightools identifiers are strings (never do arithmetic on them). Times are ISO 8601 UTC. version is the ETag (also sent as the ETag header and echoed in etag): send it as If-Match: "7" on decisions and manual sends; a stale version is 412.
revisions.current/lastApplied/lastTransmittedare revision summary objects (ornull), not numbers;inSyncistruewhen nothing has been transmitted yet or the current revision'sdataHashequals the last transmitted one's.diffis the path-level diff between the last transmitted revision and the current one, as an array of{path, from, to}; it isnullwhile nothing has been transmitted or the two are equal. For other comparisons useGET …/diff?from=&to=(transmitted | applied | <rev>→current | <rev>) orGET …/revisions/{rev}(diffFromPrevious).processing.readiness.checks[]entries are{id, status, detail, since?, nextCheckAt?}(§9.2);masterChecksisnot_availabletoday (CR-18).documents.items[]are the clean, non-deleted documents with the three fixed level texts (§12);levelsNoticeis the fixed sentence.linksare paths under/v1.
8. Other endpoints #
All under /v1, bearer-authenticated.
| Method / path | Scope | Purpose |
|---|---|---|
GET /shipments?state=&manifestState=&actionRequired=&updatedSince=&q=&limit=&offset= | shipments:read | list, filterable (q = substring of shipmentRef / CS code / SO code); paging with limit (1–200, default 50) and offset; answers {items[], total, limit, offset} — no cursors |
GET /shipments/{id} · GET /shipments/by-ref/{ref} | shipments:read | representation (§7), ETag header |
GET /shipments/{id}/revisions · GET /shipments/{id}/revisions/{rev} | shipments:read | every revision (summary objects as in §7); one revision (by number or id) with inputJson, normalizedJson, resolution, ftImage (ids and codes written to Freightools, no bodies), diffFromPrevious[] |
GET /shipments/{id}/diff?from=&to= | shipments:read | path-level diff; from = transmitted (default) | applied | rev number, to = current (default) | rev number |
POST /shipments/{id}/manifest {action: "send" | "preview"} | manifest:send | manual send (secondary to the automatic one): 202 with the representation; 409 transmission_exists → use decisions; 409 not_ready with reasons[]; 501 preview_not_verified; 503 gate_closed; honours If-Match (412) |
POST /shipments/{id}/manifest/decisions | manifest:send | §10; 201 with the representation |
GET /shipments/{id}/transmissions[/{tid}] | shipments:read | every customs transmission; the detail row carries the redacted request/response bodies and whether the immutable snapshot exists |
GET /shipments/{id}/transmissions/{tid}/request.xml · /feedback.xml | logs:xml | raw MN_MSG1 / MN_MSG4 as text/xml (every read audited) |
POST /shipments/{id}/transmissions/{tid}/re-poll | manifest:send | queue one feedback poll now (202); paced to one per transmission per 30 s (429 + Retry-After) |
GET /shipments/{id}/feedback | shipments:read | the normalised customs feedback rows (§9.3) |
GET /shipments/{id}/actions · POST /shipments/{id}/actions/{aid}/close {note, outcome?} | shipments:read / shipments:write | open and closed actions; closing with a note (outcome: "confirmed_not_sent" only on an operator_review bound to an unknown_outcome transmission) |
POST /shipments/{id}/documents · GET /shipments/{id}/documents · GET …/documents/{did} · GET …/documents/{did}/download · DELETE …/documents/{did} {reason} | documents:write / documents:read | §12; the download is a 302 to a 5-minute link; delete is a soft delete (204) |
POST /shipments/{id}/documents/{did}/push-to-freightools | documents:write | not available yet: always 501 not_implemented |
GET /shipments/{id}/audit | shipments:read | {events[], audit[]} — who did what on this shipment |
GET /shipments/{id}/tracking | shipments:read | the shipment's journey as Freightools tracks it: status (live when Freightools reports a vessel position, estimated when the position is interpolated along the route between the last actual and the next estimated milestone, no_data), position / estimatedPosition (lat, lng, bearing), route {travelled[], remaining[]} ([lat, lng] points), milestones[] (kind, port name, LOCODE, coordinates, estimated / actual times), vessel, voyage or flight, dates (ETD/ATD/ETA/ATA), phase, fetchedAt, nextFetchAt. Refreshed from Freightools every 30–60 min while in transit. 404 for another environment's shipment |
GET /reference/countries · /reference/ports?q=<LOCODE> · /reference/airports?q=<LOCODE> · /reference/container-types · /reference/package-types · /reference/incoterms · /reference/il-sites · /reference/document-types · /reference/states | shipments:read | the code lists the API accepts, resolved against your environment where applicable (countries, ports/airports, incoterms, sites) — §11 |
GET /org/environments · GET /org/me | org:read | your key's environment (host, gates, policies, connection status, settings — never key material) and the calling key (label, scopes, tenant). These are the only /org endpoints: users, invitations, API keys and the audit log are managed in the portal |
GET /openapi.json · GET /docs · GET /healthz | — | public |
9. States — what each means for you #
Full vocabularies are in the OpenAPI document; the ones you must act on:
9.1 processing.state (current revision vs Freightools) #
| State | Meaning | Your action |
|---|---|---|
received | revision stored, apply not started | none |
invalid | the latest submission failed validation; the previous revision stays | fix and resend |
applying | Freightools writes in progress | poll |
partial_failure | some writes verified, one rejected (per-step progress in the portal) | nothing to resend; processing resumes on the next revision or when ops resolves it. Never submit a new shipmentRef for the same shipment |
unknown_outcome | a write timed out; reconciliation is running | wait |
apply_failed | Freightools definitively rejected a write; the message (redacted) is in the representation | usually a data problem → resend with a fix |
blocked | a readiness check is blocked: the environment's Freightools write gate is closed (send_gates: "Freightools writes are disabled for this environment"), the air manifest path (air_manifest_path), or a customs prerequisite in an impossible shape | none possible from your side; the apply resumes by itself when the gate is opened |
waiting | applied, waiting for external prerequisites (§9.2) | this is not success and not an error; keep polling — and when the check that waits is manifest_data, send what it names in a revision (§3.1.1) |
ready | all prerequisites met | the send follows automatically when submitManifest is true and the gates are open |
9.2 Readiness checks (processing.readiness.checks[]) #
Each check is ok | waiting | unknown | blocked with a detail and, when waiting, nextCheckAt. Sea: manifest_number, master_transaction, house_transaction (filled by Freightools' customs lookup — the shipment waits; you are never asked for them), delivery_order (warning by default), il1_unique_on_manifest, execution_step, the four master checks (iso_type_vs_master, master_serials, unloading_site_vs_master, dg_flag_vs_master — always unknown: no public path to the carrier master), send_gates. Air: air_manifest_path (blocked until the Freightools air process steps carry a customs integration tool, CR-AIR), air_mawb_under_manifest (unknown). Imports also carry manifest_data: waiting while the shipment still lacks something the customs manifest needs — the consignee or its tax id, the shipper, an address of either (unless their Freightools partner already holds it), the customs discharge site (LCL: also the goods-receipt site), a weight, HS codes, the master or house document number; air: the airline and the flight; FCL: container numbers, seals or packages; LCL: the shared container or its number (the detail names them; send them in a revision, §3.1.1). An export or cross-trade has a single check, trade_scope (ok: no Israeli customs manifest), and is ready once applied. The watcher re-reads Freightools on a cadence from ETA−8 days to ETA+14 days; after that an operator_review action is opened. The shipment stays valid and is never asked to be resubmitted.
9.3 manifest.state and manifest.lastOutcome #
manifest.state | Meaning |
|---|---|
not_requested | submitManifest false, or no transmission yet (autoSend tells you whether one is armed) |
scheduled / sending / awaiting_feedback | the send is queued / calling Freightools / sent, waiting for the customs answer |
unknown_outcome | the send call timed out and no Freightools log row has appeared; never auto-resent (a duplicate would be a customs correction); ops resolves it |
accepted | customs status 1, no errors — the only success state, and only from a correlated MN_MSG4 |
accepted_with_exceptions | status 1 with errors (e.g. 4852 container type vs master, 3204 delivery order); errors mapped to your JSON paths with hints |
received_with_errors | status 2 → fix_input action with paths |
rejected | status 9 / transport error |
correction_pending_approval / documents_required | statuses 4 / 11 (+2570 lists the document types customs wants: 707, 98, IL_287) |
correction_rejected | status 12 |
updated_not_retransmitted | you changed the data after a transmission; Freightools is updated, customs is not; a customs_decision action is open (§10) |
decision_in_progress / cancel_requested / cancelled / resend_waiting_il1 | a decision is executing |
feedback_timeout | no MSG4 within 72 h → operator review |
send_failed | Freightools rejected the send call |
lastOutcome is the customs CargoStatus mapped to words (accepted, received_with_errors, split_merged, correction_pending_approval, cancelled, correction_approved, closed, held, rejected, correction_approved_manually, documents_required, correction_rejected, correction_withdrawn, other). Hebrew customs texts are returned verbatim beside the English mapping.
A transmission without feedback is never shown as accepted. waiting, awaiting_feedback and unknown_outcome are not success.
9.4 manifest.autoSend #
not_requested (submitManifest false) · armed (will send as soon as ready and the gates are open, and no transmission exists) · disarmed (a transmission exists; further sends need a decision).
10. Decisions — when and how #
After a transmission, any data change results in updated_not_retransmitted and an open action {type: "customs_decision", revision, transmission, options}. Nothing is retransmitted, corrected or cancelled at customs without your explicit decision, because a resend is a customs correction and a cancellation invalidates the house transaction number.
POST /v1/shipments/{id}/manifest/decisions
If-Match: "7"
{ "action": "correction", "revision": 2, "transmission": "<transmission id>", "note": "weight corrected per revised B/L" }
revision is the current revision's number or id; transmission may be omitted (the latest non-cancel transmission is assumed). The answer is 201 with the representation (decisions[] holds the row).
action | Customs meaning | Release 1 |
|---|---|---|
keep | nothing is sent; you acknowledge the current data is not what customs holds (the banner stays until a later transmission carries it) | executes (decisions[].state = done, the customs_decision action closes) |
correction | full resend with the same manifest number + IL1 → customs statuses 4 / 6 / 10 / 11 / 12 | recorded (state = open, manifest.state = decision_in_progress); transmitted at once when your environment's customs send is on — while it is off nothing is transmitted, the decision stays open and an operator_review task names the switch that is off |
cancel_and_resend | cancellation (status 5), wait for a new IL1, resend | recorded; transmitted at once when customs send is on — the same operator task while it is off |
cancel_only | cancellation without resend | recorded; the same as cancel_and_resend |
refer_to_operator | opens an operator task; nothing is sent | executes |
409 decision_stale: the revision is no longer current or the transmission is not the latest one — re-read the shipment and decide again. A new data revision while a decision executes finishes the running steps and opens a fresh decision. The portal posts to the same endpoint.
11. Reference endpoints #
Use them to populate pick-lists and to validate before you POST:
GET /v1/reference/countries— ISO-2 codes Freightools knows (read once per environment, cached 24 h).GET /v1/reference/ports?q=ILASH/GET /v1/reference/airports?q=ILTLV—qmust be a 5-character UN/LOCODE (free-text search is not available: Freightools resolves places by LOCODE only); zero or one record, resolved against your environment (sea ports and airports are different Freightools records even when the LOCODE is the same).GET /v1/reference/container-types— the curated ISO 6346 codes with the Freightools freight unit and the mixes each is allowed in.GET /v1/reference/package-types— UNECE Rec 21 codes with the LCL restriction flag.GET /v1/reference/incoterms— the codes your tenant has ids for.GET /v1/reference/il-sites— Israeli customs sites (sea and air) with your tenant's allow-list flag and the default commercial port each maps to.GET /v1/reference/document-types— the document types the upload accepts (§12).GET /v1/reference/states— every state vocabulary the API and the portal use (§9).
12. Documents #
Documents are uploaded separately from the shipment JSON:
POST /v1/shipments/{id}/documents multipart/form-data: documentType, note? — then ONE file part named "file"
GET /v1/shipments/{id}/documents
The documentType (and the optional note) fields must come before the file part: the upload is streamed and the row is reserved for that type before the first byte is stored, so a file part that arrives first is 400 ("documentType must precede the file part"). The answer is 201 with the document, or 200 with deduplicated: true when a clean document of the same type and sha256 already exists on the shipment. Too large is 413, a disallowed or mismatched content type 415.
Types: IL_287 cargo explanation letter, 707 B/L copy, 98 tally report (all VERIFIED from customs request 2570), 380 commercial invoice, 271 packing list (INFERRED), OTHER. Max 20 MiB (tenant-configurable). PDF, JPEG, PNG and XLSX are accepted; the type is sniffed from the file's bytes and must agree with the declared type and the extension, so a renamed executable is rejected. Files are virus-scanned when the deployment has a scanner (SCANNER=clamd; with SCANNER=none they are stored as clean at once); only clean files are listable or downloadable. Downloads are short-lived (5-minute) links with forced attachment disposition.
Every document has three independent levels, with fixed wording that the API and portal use verbatim:
- Stored here — in our bucket, scanned, linked to the shipment and the revision at upload. Release 1.
- Copied to Freightools (file) — pushed to the Freightools file store (
SetFile). Not available yet: awaits Freightools documenting the mechanism (set E6). - Transmitted to customs — sent through AttachmentToManifest. Later phase.
Release 1 therefore states on every documents screen and in every documents response: "Uploading a document stores it here only. Customs transmission of documents is not available yet." A documents_required action (customs 2570) tells you which types customs asked for; satisfying it at customs is still a manual step in release 1.
13. Rate limits #
60 requests per minute per API key (a token bucket that refills continuously; unauthenticated callers share a bucket per IP). The service itself is paced against Freightools (one pacer per Freightools license key, about 4 calls/s), so the effective throughput of applies is bounded by Freightools, not by your POST rate. A refused request is 429 rate_limited with Retry-After (also retryAfterSeconds in the envelope); back off and retry — a retry of the same body is always safe (§2.2). Validation-only traffic (dryRun) counts against the same limit.
14. Webhooks (planned) #
Release 1 is poll-based: GET /v1/shipments?updatedSince=<ISO time> returns what changed. A POST /v1/webhooks registration (events: revision applied, readiness changed, transmission sent, feedback received, action opened) is planned and will be announced in this guide and the OpenAPI document before it is enabled. Do not depend on it yet.
15. Versioning and compatibility #
- The path prefix
/v1is the contract version. Additive changes (new optional fields, new warning or deviation codes, new states listed in the OpenAPI enums) happen inside/v1without notice beyond the changelog; treat unknown codes as "unknown, show the message". - Breaking changes (a field removed or re-typed, a state removed) go to
/v2, with/v1kept for at least six months. - The OpenAPI document at
/openapi.jsonis generated from the same schemas that validate your body, so it is always the authoritative shape. - Freightools-dependent behaviour (marked FT-DEPENDENT above) may change when Freightools changes; such changes are documented in
knownDeviations[]codes before the behaviour switches.
16. Checklist for a first integration #
- Get an
em_test_…key withshipments:read shipments:writefrom the portal (Settings → API keys). POST /v1/shipmentswith example 4.1 and"options": { "dryRun": true }; readwarnings[]andknownDeviations[].- Remove
dryRun; expect201. If your test environment's write gate is still closed the shipment is recorded andprocessing.statestaysblocked(readinesssend_gates) until your contact opens it — there is no error to handle. - Poll
GET /v1/shipments/{id}untilprocessing.stateiswaitingorready; look atfreightools.customerShipment.codein Freightools. - POST the same body again →
200+Idempotent-Replayed: true. ChangegrossKg→202, then look atrevisions.current.changedPathsandGET …/revisions/2(diffin the representation is only filled after a transmission). - Handle, in your UI, the states
waiting,accepted_with_exceptions,received_with_errors,updated_not_retransmittedand thecustoms_decisionaction.