Skip to content
Reference

SCMTR.io API and webhooks

SCMTR.io's API and webhooks — connecting your own system

Last updated

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 _ACK file 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):

ScopeAllows
checkPOST /api/v1/check — check a file
files:writePOST /api/v1/files — send a file in
files:readGET /api/v1/files/{id}, with ?download=1 for the file itself
calls:readGET /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…"
}
CodeStatusMeaning
unauthenticated401No Authorization: Bearer header, or not a key SCMTR.io issued.
key_revoked401The key was revoked or has expired. Make a new one.
plan_required403API access is not switched on for the organisation. Write to support@scmtr.io.
insufficient_scope403The key lacks the permission this call needs. details.required, details.granted.
key_owner_inactive403The Org Admin who made the key is no longer one.
address_not_allowed403The organisation's IP allow-list does not include the calling address. details.address.
terms_not_accepted403The key's maker must sign in and accept SCMTR.io's current terms.
not_found404No such file or call in your organisation — another organisation's id is also not_found.
invalid_request400Empty body, not JSON, a bad parameter or an unsupported encoding.
file_too_large413Past the size limit, or more rows than any real file has.
rate_limited429Over 60 requests a minute for the key, or 120 a minute from one address. Retry-After.
internal_error500Our fault. Nothing half-done is left behind; quote the request id.
unavailable503Busy 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": "…"
}
  • kind is what the file is: DECLARATION (a CSN), ACK, STRUCTURAL_FAILURE, MANIFEST, MANIFEST_ACK, INLAND_DECLARATION (a CFS's or transhipper's report) or UNREADABLE.
  • tone is good (nothing stops it), bad (something customs would refuse), warn (worth a look) or neutral (nothing to judge).
  • problems are the red findings and notes the amber ones, each with where it is and, where there is one, how to fix it. Both lists are capped; when problemsCapped or notesCapped is true, totalProblems and totalNotes are at least that many.
  • The file is kept under your organisation, as every file sent to SCMTR.io is. file is null, with notKeptReason, 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:

typeoutcomeMeaning
csn-acknowledgementrecordedThe filing's status, and its CSN when accepted, now show it.
csn-acknowledgementnot-recordedIt answers the filing, but nothing changed — note says why.
csn-acknowledgementchoose-filingSeveral filings could be the one; candidates lists them. Send it again with ?filing=.
csn-acknowledgementno-matching-filingNo filing here answers it. Kept all the same.
csn-acknowledgementrefusedIt does not answer the filing named, or is not an acknowledgement ICEGATE sent. note.
manifest-replyrecorded / already-recordedKept with its send (transmissionId), with customs' verdict (accpStatus).
manifest-replychoose-sendSeveral sends could be the one; candidates. Send it again with ?manifestTransmission=.
manifest-replyno-matching-sendNo manifest sent from SCMTR.io answers it — one filed elsewhere. Kept all the same.
manifest-replyrefusedIt 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
limit1–100, default 25
cursorthe previous page's nextCursor
qsearch 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.recorded and check.finished carry identifiers and outcomes (and a manifest reply's file name); an alert.* 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-Delivery header, which is the same on every retry of one delivery. Events may arrive out of order; use createdAt.
  • 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 https addresses 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?

EventWhen
csn.reply.recordedA 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.recordedA manifest reply was recorded onto its send.
check.finishedA file checked through POST /api/v1/check and kept.
alert.bill-not-on-manifestThe ship is due within two days, or has arrived, and a bill on your CSN is not on the line's manifest.
alert.manifest-disagreesThe line's manifest carries your bill with different packages, containers or date.
alert.bill-on-manifestThe manifest, or an accepted amendment, now carries your bill.
alert.csn-left-recordA CSN you filed is no longer on customs' record.
alert.csn-dueA 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-movedThe shipping line now gives another arrival day for a master bill on a CSN you filed.
alert.vessel-changedThe shipping line puts a master bill on a CSN you filed on another ship for its leg into India.
alert.vcn-replacedThe 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-explainedThe line-by-line reading of a refused manifest reply is ready.
alert.manifest-preflight-checkedEvery line of a manifest that quotes a CSN has been compared with customs' record, before it is sent.
alert.line-sam-sharedA 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-checkedEvery line of a SAM or SDM you shared has been compared with the CSNs on it.
alert.line-sam-namedA shipping line that shared its SAM or SDM named your PAN on one of its bills.
alert.handshake-viewedA CSN reference link you sent was opened for the first time.
alert.handshake-file-receivedThe recipient of a CSN reference link dropped a file back.
alert.handshake-requestedSomeone else checked a CSN naming a bill you hold an accepted CSN on.
alert.counterparty-repliedThe line or forwarder you sent a case link or CSN reference card to answered on it.
alert.case-updatedA case on one of your calls changed hands or status.
alert.csn-references-foundA search for the CSN references your manifest lines are missing has finished.
alert.batch-readyA batch of CSNs has been checked and is ready to sign.
alert.filing-preparedSCMTR.io released a filing it prepared for your organisation, ready for you to sign and send.
alert.change-requestSomeone asked for a change to a SAM or SDM you file — or withdrew one.
alert.change-request-answeredThe shipping line approved or declined a change you asked for, or put it in its working copy.
alert.change-request-sentThe SAA or SDA carrying a change you asked for has been sent to ICEGATE.
alert.change-requests-readyAt your chosen time, approved changes for a vessel are ready to go in one SAA.
alert.sam-line-sent-backA line you made on a SAM your team is preparing was sent back to you.
alert.sam-ready-to-signEvery line of a SAM your team is preparing is checked, and it may be sent.
alert.sam-bills-readThe BLs you dropped on a SAM have been read.
alert.agent-requestA filing agent asks to file for your organisation.
alert.agent-request-answeredA company you asked to file for accepted or declined.
alert.agent-terms-changedYour filing agent, or a client, changed what the agent may do.
alert.agent-engagement-endedA filing agent's access to a client ended.
alert.join-requestSomeone asks to join your organisation.
alert.webhook-disabledOne of your endpoints was switched off after repeated failures.
webhook.testSent 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.