Skip to main content

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 systemImplementsITS calls it to
National customs systemPOST {base_url}/{version}/etd/notificationsAnnounce that a declaration your country is party to has a new version
Guarantee platformGET {base_url}/{version}/guarantee/{reference}Ask whether a guarantee reference is good
Cargo tracking platformGET {base_url}/{version}/tracking/{e_seal_id}Ask where the consignment carrying an electronic seal is
Cargo tracking platform, contract v3POST {base_url}/{version}/tracking/positions and GET {base_url}/{version}/tracking/{e_seal_id}/historyAsk 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 versionAdds
v1Guarantee and single-seal tracking reads
v2Movement announcements (POST /etd/notifications)
v3Bulk 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:

FieldMeaning
base_urlYour host and any gateway prefix, not one operation's path. ITS appends the contract's paths to it.
auth_modeOIDC_CLIENT_CREDENTIALS or NONE
client_idThe client ID you issued to ITS. Required for OIDC_CLIENT_CREDENTIALS.
token_urlYour token endpoint itself, not an issuer. ITS does not run OIDC discovery. Required for OIDC_CLIENT_CREDENTIALS.
contract_versionv2 or later to receive announcements
note

A step-by-step onboarding guide for the workspace is coming.

  • NONE is 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 your token_url with the client ID you issued it, and sends it as a bearer token. Both client_id and token_url are required, or the declaration is refused with VAL-ETD-065.
  • No secret travels with the declaration, and none is returned.
  • The Secretariat approves it. Your endpoint's status moves from DECLARED to APPROVED. 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"
}
FieldMeaning
serial_noThe declaration. Fetch it with GET /etd/{serial_no}.
version_noThe 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_hashThat version's hash. It tells a version you have from one you need, and it is the If-Match for an amendment.
change_typeISSUANCE, AMENDMENT, ENDORSEMENT, STATE_CHANGE or ATTACHMENT. Not a state: read the document for that.
change_categoriesFor an amendment, the sections it touched, such as ABD. Absent otherwise.
verification_codeThe declaration's public verification code. Constant across versions.
acting_countryThe country the act was performed for. Never your own. Absent when ITS itself closed a transit whose destination is not on ITS.
occurred_atWhen 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 answerITS does
2xx, ideally 202Records the announcement as sent. A Location header is kept as your receipt.
408 or 429Retries with backoff
Any other 4xxStops. The announcement will never be accepted in this shape.
5xx, a timeout or a refused connectionRetries 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:acknowledge permission and an Idempotency-Key.
  • Who is acknowledging comes from your token. There is no recipient field in the request.
  • ack_ref is 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 200 with already_acknowledged: true and 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 404 RES-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"
}
}
  • valid is the verdict and must be present. A 200 without it counts as no answer.
  • 404 means you never issued this reference. ITS records the guarantee as INVALID, which is a reason to stop a consignment.
  • 503 means you cannot answer right now. Never use 404 for "my database is unreachable".
  • surety_name is 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_available honestly. Answer 200 with source_available: false when your feed is down. ITS then shows no position rather than a stale one. A missing source_available counts as false.
  • Send occurred_at with the offset where the position was observed. ITS passes it on unchanged.
  • ITS normalises seal_status to ACTIVE, DEACTIVATED, TAMPERED or UNKNOWN. A word it does not recognise becomes UNKNOWN.
  • Do not send a field called serial_no. The contract does not define one.

With contract v3, a tracking platform also implements:

  • POST /tracking/positions with { "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 with source_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.