Receiving announcements
Most of these docs describe endpoints ITS publishes. This guide covers the other direction: the endpoints your systems publish so that ITS can call them. The partner contract is the full specification, with every schema.
What ITS calls
| Your system | Implements | ITS calls it to |
|---|---|---|
| National customs system | POST {base_url}/{version}/etd/notifications | Announce that a declaration your country is party to has a new version |
| Guarantee platform | GET {base_url}/{version}/guarantee/{reference} | Ask whether a guarantee reference is good |
| Cargo tracking platform | GET {base_url}/{version}/tracking/{e_seal_id} | Ask where the consignment carrying an electronic seal is |
Cargo tracking platform, contract v3 | POST {base_url}/{version}/tracking/positions and GET {base_url}/{version}/tracking/{e_seal_id}/history | Ask about many seals at once, and for the track you already keep |
Implement only the paths that belong to your system. A tracking platform holds no guarantee data, a guarantee platform holds no positions, and neither receives declarations. If they sit behind one national gateway, that gateway routes the paths: ITS knows one base URL per country.
The first path segment is the contract version recorded for your country: v1, v2 or v3. Countries on different versions work side by side.
| Contract version | Adds |
|---|---|
v1 | Guarantee and single-seal tracking reads |
v2 | Movement announcements (POST /etd/notifications) |
v3 | Bulk positions and seal history, for cargo tracking platforms |
A country registered at v1 is never sent announcements. It reads its notifications feed and its digest instead.
Declare your receiving endpoint
This is the one thing ITS cannot find out for itself. Until you declare it, announcements about your declarations wait, visibly, as NO_ENDPOINT.
Declare it in the ITS portal's System integration workspace:
| Field | Meaning |
|---|---|
base_url | Your host and any gateway prefix, not one operation's path. ITS appends the contract's paths to it. |
auth_mode | OIDC_CLIENT_CREDENTIALS or NONE |
client_id | The client ID you issued to ITS. Required for OIDC_CLIENT_CREDENTIALS. |
token_url | Your token endpoint itself, not an issuer. ITS does not run OIDC discovery. Required for OIDC_CLIENT_CREDENTIALS. |
contract_version | v2 or later to receive announcements |
A step-by-step onboarding guide for the workspace is coming.
NONEis not "no security". It means your ingress enforces mutual TLS, so a peer that has already proved who it is gains nothing from a bearer token.- With
OIDC_CLIENT_CREDENTIALS, ITS gets a token from yourtoken_urlwith the client ID you issued it, and sends it as a bearer token. Bothclient_idandtoken_urlare required, or the declaration is refused withVAL-ETD-065. - No secret travels with the declaration, and none is returned.
- The Secretariat approves it. Your endpoint's status moves from
DECLAREDtoAPPROVED. ITS calls only an approved endpoint. - A replacement supersedes the old one. It never edits it, so there is always a record of where ITS was calling at any past moment.
- A REC can receive for the members it files for. The recipient is still the country. Only the address belongs to the REC.
Announcements
ITS announces every change to a declaration — lodgement, each stamp, each amendment, each attachment, a cancellation — to the countries the declaration concerns. Those are the departure and destination countries, every country that has stamped it, and the country named next.
- A country is never told about its own acts. It already has the response to its own request.
- A REC is never a recipient. The country is the party and is who gets told.
- The announcement is not the document. It says a version exists. You fetch the version through
GET /etd/{serial_no}, which applies the same visibility rules as every other read.
What ITS sends
POST {base_url}/v2/etd/notifications
Content-Type: application/json
X-Correlation-ID: <the correlation id of the act, where there is one>
{
"serial_no": "UGEBBTD2608151",
"version_no": 3,
"content_hash": "9f2c8b1e04a7...",
"change_type": "ENDORSEMENT",
"verification_code": "0J73MNK1M7Y2BJPG7BDPH6GM8R",
"acting_country": "UG",
"occurred_at": "2026-08-15T08:15:30.123456Z"
}
| Field | Meaning |
|---|---|
serial_no | The declaration. Fetch it with GET /etd/{serial_no}. |
version_no | The version this announcement is about. After a retry, announcements can arrive out of order: treat a version lower than one you hold as old news. |
content_hash | That version's hash. It tells a version you have from one you need, and it is the If-Match for an amendment. |
change_type | ISSUANCE, AMENDMENT, ENDORSEMENT, STATE_CHANGE or ATTACHMENT. Not a state: read the document for that. |
change_categories | For an amendment, the sections it touched, such as ABD. Absent otherwise. |
verification_code | The declaration's public verification code. Constant across versions. |
acting_country | The country the act was performed for. Never your own. Absent when ITS itself closed a transit whose destination is not on ITS. |
occurred_at | When ITS recorded the act, in UTC |
The announcement carries no party, commodity, value or guarantee amount.
When acting_country is absent, ITS closed the transit itself. Read the declaration and check discharge_kind before you act. Never acquit a guarantee on a SYSTEM_DESTINATION_NOT_ONBOARDED closure. See Reading and searching.
How to answer
Answer quickly, then do the work. ITS allows a generous read timeout for this call, but a system that processes the whole declaration before answering will eventually be retried while it is still working on the first copy.
| You answer | ITS does |
|---|---|
2xx, ideally 202 | Records the announcement as sent. A Location header is kept as your receipt. |
408 or 429 | Retries with backoff |
Any other 4xx | Stops. The announcement will never be accepted in this shape. |
5xx, a timeout or a refused connection | Retries with backoff, up to its attempt budget, then marks the announcement DEAD |
Use X-Correlation-ID to tie a redelivery to the first one. A retry is not a new act.
Acknowledge what you processed
Answering 2xx says you received the announcement. Acknowledging says you processed it. Your delivery cursor moves only when you acknowledge.
curl --fail-with-body --silent --show-error \
--cert "$ITS_CLIENT_CERT" --key "$ITS_CLIENT_KEY" \
--header "Authorization: Bearer $ITS_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: 0f4b2c6e-1d8a-4e37-b5f9-7a3c2e1d0b94" \
--data '{ "data": { "version_no": 3, "ack_ref": "KRA-INT-99213" } }' \
"$ITS_GATEWAY_URL/etd/UGEBBTD2608151/deliveries/ack"
{
"data": {
"serial_no": "UGEBBTD2608151",
"version_no": 3,
"recipient": "KE",
"already_acknowledged": false,
"acknowledged_at": "2026-08-15T08:19:02.88Z",
"cursor": 3,
"ack_ref": "KRA-INT-99213"
}
}
- It needs the
etd:acknowledgepermission and anIdempotency-Key. - Who is acknowledging comes from your token. There is no
recipientfield in the request. ack_refis your own reference, echoed back. Put your internal message id there, so a support conversation has one identifier both sides can see.- Acknowledging twice answers
200withalready_acknowledged: trueand the first acknowledgement's time. - The cursor only moves forward. An acknowledgement that arrives out of order never rewinds it.
- A REC and the Secretariat cannot acknowledge: neither has a country of its own.
- No announcement of that version for your country is
404RES-ETD-030.
Catch up with the digest
GET /etd/reconciliation/digest?since=2026-08-01T00:00:00Z&limit=200
The digest lists the announcements sent to your country that you have not acknowledged. Run it on a schedule, even with a receiving endpoint. It is the backstop for announcements your system missed, including any marked DEAD.
{
"data": {
"country": "KE",
"outstanding": [
{
"serial_no": "UGEBBTD2608151",
"version_no": 3,
"change_type": "ENDORSEMENT",
"content_hash": "938501f2...",
"sent_at": "2026-08-15T08:15:31.10Z"
}
]
}
}
The digest is measured against what was sent to you, never against what exists. Because you are not told about your own acts, a declaration you acted on is not a gap.
Who has been told
The country that filed a declaration, and the Secretariat, can see how each announcement went:
GET /etd/{serial_no}?include=deliveries
Each row has a recipient, a version_no, a status (PENDING, SENT, ACKED, FAILED or DEAD), attempts, sent_at, acked_at, ack_ref and the last error.
NO_ENDPOINT with attempts: 0 is not a failure. It means that country has not declared a receiving endpoint yet. The announcement waits and is delivered on the day the endpoint is approved. PARTY_SECRET_MISSING waits the same way. Everybody else gets the 404 an unissued serial gets.
Guarantee checks
ITS asks the departure country's guarantee platform whether the reference in field 20 is good, each time someone reads the declaration's guarantee.
GET {base_url}/{version}/guarantee/{reference}
{
"status": "success",
"data": {
"guarantee_reference_no": "CTG-KE-2026-0001",
"valid": true,
"amount": 50000.0,
"currency": "USD",
"valid_until": "2026-12-31",
"surety_name": "Example Surety Ltd",
"status": "ACTIVE"
}
}
validis the verdict and must be present. A200without it counts as no answer.404means you never issued this reference. ITS records the guarantee asINVALID, which is a reason to stop a consignment.503means you cannot answer right now. Never use404for "my database is unreachable".surety_nameis shown to customs officers only, never on a public screen.
Until your country registers a guarantee platform, every guarantee on your declarations reads DECLARED_UNVERIFIED.
Cargo tracking
ITS asks the tracking platform of the country that fitted the electronic seal currently on the consignment. The key is the electronic seal identifier, e_seal_id, never an ITS serial number.
GET {base_url}/{version}/tracking/{e_seal_id}
{
"status": "success",
"data": {
"e_seal_id": "60D2101014",
"source_available": true,
"position": { "latitude": -3.3758333, "longitude": 29.34763 },
"location": { "name": "0.40 KM N of Gare routiere Truck Parking" },
"occurred_at": "2025-12-09T12:14:51+03:00",
"seal_status": "Active",
"speed_kph": 24,
"heading_degrees": 314
}
}
- Set
source_availablehonestly. Answer200withsource_available: falsewhen your feed is down. ITS then shows no position rather than a stale one. A missingsource_availablecounts asfalse. - Send
occurred_atwith the offset where the position was observed. ITS passes it on unchanged. - ITS normalises
seal_statustoACTIVE,DEACTIVATED,TAMPEREDorUNKNOWN. A word it does not recognise becomesUNKNOWN. - Do not send a field called
serial_no. The contract does not define one.
With contract v3, a tracking platform also implements:
POST /tracking/positionswith{ "e_seal_ids": [...] }, at most 500 seals per request. Answer for every seal you were asked about: a seal you do not recognise is an entry withsource_available: false, not a missing entry. ITS calls this on a timer.GET /tracking/{e_seal_id}/history?from=...&to=..., the track you hold in that window, oldest first. ITS calls this only when a user asks to see a route. Thin the track before you send it.
How ITS calls your guarantee and tracking reads
These reads happen while a truck waits at a barrier, so:
- connect and read timeouts are 2 seconds, with a 2.5-second deadline for the whole call;
- there are no retries: a failed read is reported as a failed read;
- after five consecutive failures, ITS stops calling your system for 30 seconds, then tries once. Your outage never affects another country's answers.
Every failure becomes an honest status: VERIFICATION_UNAVAILABLE for a guarantee and TRACKING_UNAVAILABLE for tracking. ITS never reports a guarantee as VERIFIED, or shows a position, because a call failed.