SCMTR.io API and webhooks
SCMTR.io's API and webhooks — connecting your own system
Can we connect our own system to SCMTR.io?
Yes. SCMTR.io has an API, so your own software — a forwarder's back office, a shipping line's operations system, an ERP — can work with SCMTR.io without a person at a browser. Through it your system can:
- Check a file before it goes to ICEGATE — a CSN, a SAM / SDM / SAA, an acknowledgement or a manifest reply — and get back the same verdict Check an ICEGATE file gives in the app: what customs would refuse, what is worth a look, and how to fix each.
- Send files in — your filings and customs' replies. A reply is recorded onto the CSN or the
manifest send it answers, exactly as dropping the
_ACKfile in the app records it, and every file is placed on its vessel call. - Read your VCNs — each vessel call your organisation's filings and files are on, the bills on it, and where each message on it stands (sent, accepted, refused).
- Read the files kept for you, and download them as they were received.
And with webhooks, SCMTR.io tells your system the moment something happens — a CSN acknowledgement recorded, a manifest reply recorded, a bill missing from the line's manifest, a line's SAM that differs from your CSN, a CSN due before the line's cut-off — so your software does not have to keep asking.
To start, write to support@scmtr.io and we switch the API on for your organisation; your Org Admin then makes the keys in Settings → API. Signing and sending a filing to ICEGATE stay in the app.
This page is about SCMTR.io's own API. For ICEGATE's API — filing to customs machine to machine — see Filing through the ICEGATE API.
How do we get API access?
Write to support@scmtr.io from your organisation and say what you want to connect. We switch the API on for your organisation. After that, an Org Admin creates keys and webhook endpoints in Settings → API; nobody else in the organisation can.
Until it is switched on, Settings → API says the organisation's plan does not include API access.
What can our system not do through the API?
- Send a filing to ICEGATE. Signing with your DSC and sending stay in SCMTR.io, where a person confirms each send (Sign & upload). Your system can make the file and check it through the API; a person signs it and sends it.
- Make or change a filing. A CSN or manifest your system sends in is kept and placed on its vessel call; it does not become a draft, and it does not change a filing made here.
- Ask customs' record. The API's check reads the file itself. Checking a CSN's bills, or a manifest's lines, against what customs holds is done in the app.
- See anybody else's filings. A key reads your own organisation's filings, files and calls only — never another filer's, not even a count of them.
Is a key safe? Who is it acting as?
- A key acts as the Org Admin who made it, in that one organisation, and everything it does is in the audit log under that person. A key never reaches another organisation that person also belongs to. If they stop being an Org Admin there, the key stops working.
- A key is shown once. SCMTR.io cannot show it again; if it is lost, revoke it and make another.
- A key can expire after 30, 90 or 365 days, or never, and can be revoked at once in Settings → API.
- A key is given only the permissions it needs — check files, send files in, read kept files, read VCNs — chosen when it is made.
- Your organisation's IP allow-list applies (Settings → Enterprise): when one is set, a call from any other address is refused, as it is for your people on the website and the phone app.
- Every file downloaded through a key is recorded, naming the key.
For your developers: authentication, permissions and limits
Base address: https://scmtr.io/api/v1. Every answer is JSON except a file download. Send the key as a
bearer token:
Authorization: Bearer scmtr_live_…A key looks like scmtr_live_ followed by 43 characters. Each key is given one or more of these
permissions (scopes):
| Scope | Allows |
|---|---|
check | POST /api/v1/check — check a file |
files:write | POST /api/v1/files — send a file in |
files:read | GET /api/v1/files/{id}, with ?download=1 for the file itself |
calls:read | GET /api/v1/calls and GET /api/v1/calls/{id} — your VCNs |
Give each integration only what it needs: a check in a back office needs check and nothing else.
Limits. 60 requests a minute per key; past it, 429 rate_limited with Retry-After: 60. A file to
check may be up to 16 MB for a manifest or a manifest reply and 1 MB for anything else; a file sent in,
up to 32 MB. A body may be sent gzipped (Content-Encoding: gzip) — the limit is on the file unzipped.
When SCMTR.io is busy reading other large files the answer is 503 unavailable with Retry-After: 5:
nothing was kept, send it again.
Request ids. Every answer carries x-request-id and a requestId field. Send your own
x-request-id (8–80 letters, digits, ., _ or -) and it is used instead. Quote it when you write to
us.
For your developers: errors
Every failure has the same shape and a code that never changes, so your integration can branch on
it. The message is for your logs and may be reworded.
{
"error": { "code": "insufficient_scope", "message": "This key may not do that…", "details": { "required": "files:read", "granted": ["check"] } },
"requestId": "6f1c…"
}| Code | Status | Meaning |
|---|---|---|
unauthenticated | 401 | No Authorization: Bearer header, or not a key SCMTR.io issued. |
key_revoked | 401 | The key was revoked or has expired. Make a new one. |
plan_required | 403 | API access is not switched on for the organisation. Write to support@scmtr.io. |
insufficient_scope | 403 | The key lacks the permission this call needs. details.required, details.granted. |
key_owner_inactive | 403 | The Org Admin who made the key is no longer one. |
address_not_allowed | 403 | The organisation's IP allow-list does not include the calling address. details.address. |
terms_not_accepted | 403 | The key's maker must sign in and accept SCMTR.io's current terms. |
not_found | 404 | No such file or call in your organisation — another organisation's id is also not_found. |
invalid_request | 400 | Empty body, not JSON, a bad parameter or an unsupported encoding. |
file_too_large | 413 | Past the size limit, or more rows than any real file has. |
rate_limited | 429 | Over 60 requests a minute for the key, or 120 a minute from one address. Retry-After. |
internal_error | 500 | Our fault. Nothing half-done is left behind; quote the request id. |
unavailable | 503 | Busy reading large files, or briefly unable to check the request limits. Retry after Retry-After, or shortly. |
For your developers: checking a file — POST /api/v1/check
Scope check. The body is the file, byte for byte — a signed CSN's signature is checked over the exact
bytes. You may name the file in the X-Scmtr-File-Name header, URI-encoded (a header rather than the
address, because a file name is often a job number and date that are nobody else's business).
curl https://scmtr.io/api/v1/check \
-H "Authorization: Bearer $SCMTR_KEY" \
-H "X-Scmtr-File-Name: F_SACHM22_SCE_SENDER01_1347_20260814_DEC.json" \
--data-binary @F_SACHM22_SCE_SENDER01_1347_20260814_DEC.json{
"check": {
"kind": "DECLARATION",
"tone": "bad",
"headline": "2 problems customs would refuse",
"summary": "…",
"problems": [{ "title": "…", "where": "Master consignment 1 › Transport document", "detail": "…", "steps": ["…"] }],
"totalProblems": 2,
"problemsCapped": false,
"notes": [],
"totalNotes": 0,
"notesCapped": false,
"facts": [{ "label": "Reporting event", "value": "SCE — Sea Cargo Entry" }, { "label": "Signature", "value": "Not signed" }],
"dsc": null
},
"file": { "id": "0b9e…", "duplicate": false },
"requestId": "…"
}kindis what the file is:DECLARATION(a CSN),ACK,STRUCTURAL_FAILURE,MANIFEST,MANIFEST_ACK,INLAND_DECLARATION(a CFS's or transhipper's report) orUNREADABLE.toneisgood(nothing stops it),bad(something customs would refuse),warn(worth a look) orneutral(nothing to judge).problemsare the red findings andnotesthe amber ones, each with where it is and, where there is one, how to fix it. Both lists are capped; whenproblemsCappedornotesCappedis true,totalProblemsandtotalNotesare at least that many.- The file is kept under your organisation, as every file sent to SCMTR.io is.
fileisnull, withnotKeptReason, when it could not be kept — the check still ran.
The check is for information: customs decides, and only its reply says what it decided.
For your developers: sending a file in — POST /api/v1/files
Scope files:write. The body is the file; X-Scmtr-File-Name as above. The answer is 201 for a new
file and 200 when the same bytes are already held, or when the file could not be kept (file is
null).
- A CSN acknowledgement is recorded onto the filing it answers when exactly one filing is certainly
the one. Add
?filing=<declarationId>to name the filing yourself; the file must still answer it. - A manifest reply is kept with the manifest send it answers. Add
?manifestTransmission=<id>when more than one send could be the one. - Anything else is kept and placed on its vessel call.
{
"file": { "id": "5c0f…", "duplicate": false },
"kind": "CSN_ACK",
"recorded": {
"type": "csn-acknowledgement",
"outcome": "recorded",
"declarationId": "a41e…",
"jobNo": 1347,
"status": "ACCEPTED",
"csnNmbr": 1234567,
"note": null
},
"requestId": "…"
}kind is the kept file's kind: CSN_DEC, CSN_DEC_SIGNED, CSN_ACK, MANIFEST_DEC,
MANIFEST_DEC_SIGNED, MANIFEST_ACK, INLAND_DEC, INLAND_ACK, STRUCTURAL_FAILURE, PDF,
SPREADSHEET, UNKNOWN or NOT_JSON. recorded is null for a file that is not a reply. For a
reply, outcome says what happened:
type | outcome | Meaning |
|---|---|---|
csn-acknowledgement | recorded | The filing's status, and its CSN when accepted, now show it. |
csn-acknowledgement | not-recorded | It answers the filing, but nothing changed — note says why. |
csn-acknowledgement | choose-filing | Several filings could be the one; candidates lists them. Send it again with ?filing=. |
csn-acknowledgement | no-matching-filing | No filing here answers it. Kept all the same. |
csn-acknowledgement | refused | It does not answer the filing named, or is not an acknowledgement ICEGATE sent. note. |
manifest-reply | recorded / already-recorded | Kept with its send (transmissionId), with customs' verdict (accpStatus). |
manifest-reply | choose-send | Several sends could be the one; candidates. Send it again with ?manifestTransmission=. |
manifest-reply | no-matching-send | No manifest sent from SCMTR.io answers it — one filed elsewhere. Kept all the same. |
manifest-reply | refused | It contradicts the send it names, or a different reply is already recorded for it. note. |
For your developers: reading files and VCNs
GET /api/v1/files/{id} (scope files:read) gives a kept file's details: its name and kind, the
message, event, sender, port, job number and date, the VCN, rotation and IMO it names, the CSN number and
customs' verdict where it carries them, its vessel call, the bills it names, when it was received, and
expiresAt — when it may be deleted under your organisation's retention period. With ?download=1 the
answer is the file itself, exactly as it was received.
GET /api/v1/calls (scope calls:read) lists your organisation's vessel calls, most recent activity
first: port, VCN, rotation, IMO, vessel name, arrival, entry inward, and how many CSNs, manifest versions
and files are on each.
| Parameter | |
|---|---|
limit | 1–100, default 25 |
cursor | the previous page's nextCursor |
q | search by VCN, rotation, vessel name, IMO, or a bill on your own filings and files (up to 100 characters) |
GET /api/v1/calls/{id} gives one call: your CSN filings on it (status, CSN number, MCIN / PCIN),
your manifest filings with each version and its reply, the files kept on it, and every bill those name
with each message that names it. Another filer's filings on the same rotation are never included, and
neither is a count of them.
For your developers: webhooks
An Org Admin adds an endpoint in Settings → API: an https address and the events to send, or *
for every event, including ones added later. The endpoint's signing secret (whsec_…) is shown once,
and a new one can be made at any time. Send test sends a webhook.test event.
- An event never carries a file.
csn.reply.recorded,manifest.reply.recordedandcheck.finishedcarry identifiers and outcomes (and a manifest reply's file name); analert.*event carries the alert as your people see it in the app — its title, its text and the bills it is about, which can name the other party on a case or the master bills concerned. Treat your endpoint's log as you treat the filings. - Delivery is at least once: de-duplicate on the
X-Scmtr-Deliveryheader, which is the same on every retry of one delivery. Events may arrive out of order; usecreatedAt. - Answer with any 2xx within 10 seconds. Anything else — a redirect (never followed), a 4xx, a 5xx, a timeout — is retried for 24 hours, at growing intervals. 50 failures in a row switch the endpoint off and your Org Admins are told; switching it back on resumes the deliveries already queued that are still within their day; events raised while it was off are not sent.
- Only
httpsaddresses on the public internet are accepted — private, local and cloud-internal addresses are refused when the endpoint is saved and again at every delivery.
A delivery looks like this:
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SCMTR-Webhooks/1 (+https://scmtr.io/reference/scmtr-api-and-webhooks)
X-Scmtr-Event: csn.reply.recorded
X-Scmtr-Delivery: 7e3b0c1a-…
X-Scmtr-Signature: t=1758182400,v1=5f2b…
{"id":"csn-reply:…","event":"csn.reply.recorded","organizationId":"…","createdAt":"2026-09-18T08:00:00.000Z",
"data":{"declarationId":"…","jobNo":1347,"accpStatus":"Accepted","status":"ACCEPTED","csnNmbr":1234567,"csnDt":"20260820"}}For your developers: which webhook events are there?
| Event | When |
|---|---|
csn.reply.recorded | A CSN acknowledgement file was recorded onto a filing — from the app, the phone or the API. A CSN read off customs' public record without its acknowledgement file sends none. |
manifest.reply.recorded | A manifest reply was recorded onto its send. |
check.finished | A file checked through POST /api/v1/check and kept. |
alert.bill-not-on-manifest | The ship is due within two days, or has arrived, and a bill on your CSN is not on the line's manifest. |
alert.manifest-disagrees | The line's manifest carries your bill with different packages, containers or date. |
alert.bill-on-manifest | The manifest, or an accepted amendment, now carries your bill. |
alert.csn-left-record | A CSN you filed is no longer on customs' record. |
alert.csn-due | A CSN you have not sent is for a ship whose CSN cut-off is near: a day before it, and once it has passed. |
alert.arrival-moved | The shipping line now gives another arrival day for a master bill on a CSN you filed. |
alert.vessel-changed | The shipping line puts a master bill on a CSN you filed on another ship for its leg into India. |
alert.vcn-replaced | The VCN a CSN or SAM you filed names has probably been replaced by a new one for the same call. Never applied to your filing. |
alert.rejection-explained | The line-by-line reading of a refused manifest reply is ready. |
alert.manifest-preflight-checked | Every line of a manifest that quotes a CSN has been compared with customs' record, before it is sent. |
alert.line-sam-shared | A shipping line shared its SAM or SDM on SCMTR.io, and one of its lines carries a bill you hold a CSN on. |
alert.line-sam-share-checked | Every line of a SAM or SDM you shared has been compared with the CSNs on it. |
alert.line-sam-named | A shipping line that shared its SAM or SDM named your PAN on one of its bills. |
alert.handshake-viewed | A CSN reference link you sent was opened for the first time. |
alert.handshake-file-received | The recipient of a CSN reference link dropped a file back. |
alert.handshake-requested | Someone else checked a CSN naming a bill you hold an accepted CSN on. |
alert.counterparty-replied | The line or forwarder you sent a case link or CSN reference card to answered on it. |
alert.case-updated | A case on one of your calls changed hands or status. |
alert.csn-references-found | A search for the CSN references your manifest lines are missing has finished. |
alert.batch-ready | A batch of CSNs has been checked and is ready to sign. |
alert.filing-prepared | SCMTR.io released a filing it prepared for your organisation, ready for you to sign and send. |
alert.change-request | Someone asked for a change to a SAM or SDM you file — or withdrew one. |
alert.change-request-answered | The shipping line approved or declined a change you asked for, or put it in its working copy. |
alert.change-request-sent | The SAA or SDA carrying a change you asked for has been sent to ICEGATE. |
alert.change-requests-ready | At your chosen time, approved changes for a vessel are ready to go in one SAA. |
alert.sam-line-sent-back | A line you made on a SAM your team is preparing was sent back to you. |
alert.sam-ready-to-sign | Every line of a SAM your team is preparing is checked, and it may be sent. |
alert.sam-bills-read | The BLs you dropped on a SAM have been read. |
alert.agent-request | A filing agent asks to file for your organisation. |
alert.agent-request-answered | A company you asked to file for accepted or declined. |
alert.agent-terms-changed | Your filing agent, or a client, changed what the agent may do. |
alert.agent-engagement-ended | A filing agent's access to a client ended. |
alert.join-request | Someone asks to join your organisation. |
alert.webhook-disabled | One of your endpoints was switched off after repeated failures. |
webhook.test | Sent only by Send test in Settings → API. |
For your developers: verifying a webhook's signature
X-Scmtr-Signature: t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of <t>.<raw body> under the
endpoint's secret. Compute it over the raw request body — never over JSON you parsed and wrote out
again — compare in constant time, and refuse a timestamp more than five minutes from now. A delivery carries
one v1=. When you make a new secret the old one stops signing at once, so update your endpoint
with the new secret as soon as you make it.
Node.js (21 or later, for Object.groupBy):
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyScmtrSignature(header, secret, rawBody, toleranceSeconds = 300) {
const parts = Object.groupBy(header.split(','), (p) => p.split('=')[0]);
const t = Number(parts.t?.[0]?.slice(2));
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return (parts.v1 ?? []).some((p) => {
const given = Buffer.from(p.slice(3), 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Python:
import hashlib, hmac, time
def verify_scmtr_signature(header: str, secret: str, raw_body: bytes, tolerance: int = 300) -> bool:
pairs = [p.split("=", 1) for p in header.split(",") if "=" in p]
ts = next((v for k, v in pairs if k == "t"), None)
if ts is None or not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in pairs if k == "v1")Will the API change under our integration?
Version 1 (/api/v1) changes only by adding — new calls, new optional parameters, new fields in an
answer, new error codes, new events. Your integration should ignore fields and events it does not know.
Removing anything, or changing what it means, would be a new version, /api/v2, announced in advance and
run beside v1.
Still stuck on this?
Ask the assistant about this page, or anything else in SCMTR filing — in English, Hinglish or your own language.
Have the file? Check it free — no sign-in
General information only — not legal or customs-compliance advice, and it may not reflect the most current ICEGATE/CBIC requirements. Verify against the official sources, or a licensed customs broker, before filing.