Licensed under GPL-3.0-or-later; see LICENSE.
Unofficial browser-free client for ICEGATE, eSANCHIT, electronic filing and read-only reports. It logs in with the same endpoints the portal's own JavaScript uses, refreshes the dashboard token, performs the dashboard to eSANCHIT handoff, uploads already-signed PDFs, and can submit one staged batch to obtain the DRN and IRN. Electronic filing uploads already-signed SB/BE files and reports the portal's initial response.
See the file-upload research for observed portal behavior and the distinction between upload acceptance and Customs processing.
Staging uploads files; submission creates portal records. icegate esanchit submit
requires a challenge string derived from the file hashes before it sends
anything. Staging is scoped to the current eSANCHIT session: the queue is empty
again on the next handoff, so upload and submit must happen in one process.
A two-PDF live submission succeeded on 2026-10-29 and its DRN and IRNs matched the confirmation email. Other response shapes may still report
unknowneven when accepted. The client never retries a submit.
The portal keeps its authentication state in sessionStorage, so a browser
restart means a fresh login, and the eSANCHIT session expires after roughly ten
minutes of inactivity. This client removes the browser from the loop:
- no browser process, profile or cookie store;
- credentials read from a local file;
- direct login with the configured ICEGATE credentials;
- a journal designed to redact secrets.
| Rule | How it is enforced |
|---|---|
| Stage never submits | FORM_ACTION_ALLOWLIST holds only uploadDocs; every POST passes an explicit allowlist |
| Submit needs a typed challenge | SUBMIT <n> <plan_id> covers the file hashes, sizes and codes; a changed file changes the challenge |
| Avoid repeat submit | one POST per attempt, a state-directory lock and a receipt per plan; these cannot detect submissions made elsewhere |
| Reduce secret exposure | journal fields and normal console output use the redactor; handoff URL query strings are removed |
| Never disable TLS verification | verify is always a CA bundle path, never False |
| Never retry a POST | only GET/HEAD are retried, once |
| Never store tokens | token and JSESSIONID live in memory for the life of the command |
Install uv and Python 3.11 or newer. From the repository directory on Linux, macOS or Windows:
uv tool install .
icegate --helpRun uv tool update-shell and restart the terminal if icegate is not on your PATH. For development, uv sync followed by uv run icegate ... also works. On Windows, uv tool install --editable . keeps the installed command pointed at the checkout, so edits apply without reinstalling.
icegate init # creates .env
# Edit .env in your preferred text editor
icegate doctorWhen run from the source checkout, the client uses the ignored icegate-client/.env by default. On Linux and macOS it must not be group- or world-readable. On Windows, mode bits do not enforce privacy: put .env and the state directory in a folder whose Windows ACL permits only your account, and verify its permissions yourself. The client does not validate Windows ACLs. An installed wheel
uses .env in the current directory by default.
ICEGATE_USERNAME=your-id
ICEGATE_PASSWORD=your-password
Environment variables with the same names override the file, which is handy for
one-off runs. ICEGATE_STATE_DIR and ICEGATE_TIMEOUT are optional.
To use another file instead of the default, pass --env-file PATH to each
command.
icegate login # authenticate and probe the dashboard
icegate status # authenticate, rotate the token, probe again
icegate esanchit handoff # open eSANCHIT and list staged rows
icegate esanchit doc-types "invoice" # search the bundled document codes
icegate esanchit stage a.pdf b.pdf --type 331000
icegate esanchit stage a.pdf --dry-run # local validation only
icegate esanchit submit a.pdf --type 331000 --dry-run
icegate esanchit submit a.pdf --type 331000 --confirm 'SUBMIT 1 <plan_id>'
icegate filing upload signed.sb --dry-run
icegate filing upload signed.sb --confirm 'FILE 1 <plan_id>'
icegate ecopy search SB --from 2026-10-01 --to 2026-10-28
icegate ecopy sb --yesterday
icegate ecopy be --today
icegate ecopy search BE --location INMBD6 --job-number 123 --job-date 2026-10-28 --json
icegate ecopy download ack --filename <downloadUrl> --output acknowledgement.ack
icegate ecopy download --irn <irn> -o signed-copy.pdf
icegate ecopy search GATEPASS --from 2026-10-27 --to 2026-10-27 --json # SB35 gatepass records
icegate report job-status --json # latest ~200 bills, every column
icegate my-iec --json # all IEC clients
icegate notifications --events # filing outcomes: submitted, rejected, validation failed
icegate enquiry sb --location INMBD6 --sb-no 6817710 --sb-date 2026-09-14 --json # public, no loginecopy sb, ecopy be, ecopy gatepass, and ecopy search retrieve Shipping Bill (SB), Bill of Entry (BE) or Shipping Bill gatepass (GATEPASS) records
by date range, or by customs location, job number and job date. Dates use
YYYY-MM-DD. Add --json for the complete portal records, including fields
for available copies; the table shows job, document and location details.
--today and --yesterday use the calendar date in India. --yestarday is
also accepted as an alias for --yesterday.
Use either a relative-date flag, a --from/--to pair, or all three job
fields; they cannot be combined. The JSON downloadUrl field is a portal
filename, not necessarily a complete URL.
The portal may restrict the permitted date range. Search only lists records. To
download, use --json to find the downloadUrl or irn for the desired row.
The client remembers each search result's portal user ID in the local state
directory, scoped to your account, and uses it automatically for signed PDF
downloads. Search for the record first; if you have not searched for it, the
command will ask you to do so. --user-id is available as an explicit override.
The saved identifiers are not login credentials or session tokens. The signed action downloads a PDF;
the acknowledgement action downloads the portal’s .ack file. On Linux and macOS files are mode 0600; on Windows they inherit folder ACLs, which you must restrict. Existing destinations are never overwritten. On 2026-10-29, live
SB and BE signed PDF downloads succeeded. The acknowledgement endpoint
returned HTTP 409 for six sampled files; availability remains uncertain. A signed
download answering 409 means the portal is still retrieving an older document;
retry in about an hour. Gatepass records without an irn have no gatepass yet.
notifications reads the dashboard bell (read-only; it never marks anything read). Plain, it
prints the raw list; with --events it prints only filing outcomes, newest first, as JSON
(outcome is submitted, rejected or failed, with job, date, location and detail).
submitted means accepted for processing, not final. Notifications have a date but no time.
report job-status prints the portal's latest ~200 Shipping Bills (no date
filter), my-iec lists your IEC clients, and enquiry sb reads the public
Shipping Bill enquiry for one bill without logging in. All three are read-only
and accept --json.
filing upload accepts .sb and .be files containing signature and
certificate sections. It checks the section structure only: it does not
cryptographically verify the DSC or sign the file. accepted_for_processing
is the initial portal response, not a Customs filing result. Check the separate
ICEGATE status email. Each file is sent once, and an attempt receipt prevents
automatic resubmission; an uncertain response must be reconciled manually.
For a batch, processing stops at the first failed or unknown response and
remaining files are not attempted. The challenge printed by the dry run must
be copied exactly into the upload command. A local receipt prevents repeat
uploads only within the same state directory. After reconciling a previous attempt,
--allow-duplicate permits an intentional repeat; it can create a duplicate filing.
submit is two runs by design. The first prints the plan and a challenge; the
second, with --confirm, authenticates, hands off, uploads, re-checks that the
portal shows exactly the planned rows, and posts one submitDocs. Exit codes:
0 verified response, 3 outcome unknown, 4 confirmation missing. Local
validation failures stop before submission.
stage performs the whole flow in one process: login, handoff, upload, then a
report. It prints the staged rows (file, start time, signature verdict, the
portal's own type field) and exits. --doc-type and --doc-type-map are
plans: the client checks each code against the row's real option list and
prints it, but the portal only persists a type when it submits.
Useful stage flags:
| Flag | Meaning |
|---|---|
--doc-type CODE |
plan one document code for every file |
--doc-type-map FILE=CODE |
per-file plan, repeatable |
--dry-run |
validate files and stop before any network call |
--append |
allow uploading when rows are already staged |
--allow-unsigned |
do not fail on missing signature markers |
--strict-limits |
treat portal size and count limits as errors |
--dump-html PATH |
save the post-upload page (private permissions on POSIX) for debugging |
Warnings and errors go to stderr; successful --json output goes to stdout.
ICEGATE expiry date strings are interpreted as India time (the portal does not
provide a timezone), independent of the computer’s timezone.
Workflow commands accept --json, --env-file, --state-dir,
--no-journal and -v.
upload "FILE_PATH" # Linux/macOS, if the upload command is installed
scripts/icegate-run.py "FILE_PATH" # Linux/macOS
uv run scripts/icegate-run.py "FILE_PATH" # Windows (no shebang)
scripts\upload.cmd "FILE_PATH" # Windows launcherReplace FILE_PATH with the path to your folder containing the files. The folder can be anywhere.
A personal workflow around the client, not part of it. Both scripts are plain Python and
system-neutral. The signer is separate: on Linux icegate-sign finds the vendor
libcastle_v2.so in ~/.local/lib/hyp2003/; on Windows the driver EXE from the token
vendor installs eps2003csp11v2.dll in System32, and sign-all.py passes it to
icegate-sign as ICEGATE_PKCS11_MODULE. Install the signer once per machine:
git clone https://github.com/quantavil/icegate-sign to ~/Documents/icegate-sign
(or set ICEGATE_SIGN_DIR); it runs in its own uv environment. --no-sign lets
the upload half run anywhere on files signed elsewhere. The script declares its own
dependencies (questionary for the prompts, rich for the tables and spinner, pymupdf to flatten PDFs) in a header, so uv installs it on first run; the client's
own dependencies are unchanged. It does this, in order:
- Sign. Each unsigned PDF is first flattened to page images (PyMuPDF, 120 dpi
grayscale; old signatures become part of the image, the original goes to
FOLDER/.original/, text is no longer selectable). Unsigned.pdf,.sband.befiles are then signed by runningscripts/sign-all.py FOLDERas a separate program (--no-signskips this). The signer alone handles the token and PIN; the script never sees them. - Pick files. A checklist of the signed files (all ticked). The ledger in the state directory remembers uploaded names and original-file hashes. A different original with a previously used name can be signed and uploaded; a known identical original is stopped. Previously uploaded signed files without a newly signed original are skipped. Older ledger entries contain names only, so the script warns that their contents cannot be compared; check the portal and email before proceeding. Signing is isolated to files that need it, so existing signed files are not replaced. If a run is cancelled after signing, it can resume while both files remain unchanged; if either changes, review and remove the stale signed copy before signing again.
- Pick the document type per PDF. A searchable list of every code in
document_codes.txt, with the best guess from the file name pre-selected, so Enter accepts it. The guess comes from the catalogue alone: a multi-word catalogue name found whole in the file name, such as "commercial invoice", or "invoice" plus "packing list" meaning 331000.INVandPLare read as invoice and packing list, soINV&PL 123gets 331000;INValone only suggests candidates. Without a terminal it uses a code only when exactly one entry fits, otherwise stops. - Confirm. It shows the selected files, PDF document types and validation summary,
then asks once;
--yesskips the question and is needed when not on a terminal. It then passes the plan's challenge to the client's--confirm. An unknown outcome stops the run with "do not re-run". - Draft and archive. It writes the reply as HTML tables (eSANCHIT from the
receipt, SB and BE from an e-copy search using the location, job number and job date read from the SB or BE file; the number can take a couple of minutes to appear,
so it re-checks every 30 s for up to
--waitseconds, default 600, and Ctrl-C skips it. Each check also reads ICEGATE's notifications: a rejection or validation failure for that job ends its wait at once, prints the portal's message and error code, and the run exits with an error after the other tables are made. Only notices that appear after the upload count, because job numbers repeat), copies each table to the clipboard as rich text the moment it exists (IRN/DRN first, then the SB and BE tables when they appear; paste in between) (wl-copyorxclipon Linux, Windows PowerShell; macOSosascriptis in the code but untested), and moves the originals, signed files and drafts intoDone/<timestamp>/inside the folder. That folder is created after every upload and is safe to delete.
To show and copy tables from a completed run, use upload "FILE_PATH" --recheck,
replacing FILE_PATH with that run's specific Done/<timestamp> folder, wherever it is located.
If both tables exist, both are printed and the
last is copied; use --table esanchit, --table sb or --table be to copy one specifically.
If an SB or BE table was not created, recheck makes one read-only lookup using the
archived file, and reports any rejection the portal's notifications hold for that job. It never signs or uploads.
Recheck performs one lookup by default; add --wait 600 to wait up to ten minutes
for a missing SB or BE number. An existing saved table is displayed without a network lookup.
To choose a recent completed run interactively, use upload "FILE_PATH" --ls.
Here FILE_PATH is the working folder or its Done folder. The picker lists runs
newest first with their file names; selecting one rechecks it and copies the table.
Use --table sb, --table be or --table esanchit to select which table to copy.
For an .sb or .be file the customs location is read from the file itself (the first field of its
<TABLE>SB or <TABLE>BE record); there is no option for it. If the files name different locations or none can be read, the run stops before anything is sent. The SB lookup also uses the job number and date in that record, regardless of the filename. A BE file whose location or job cannot be read is still uploaded, but gets no BE-number check. --dry-run lists signed and unsigned files and guessed codes without signing or uploading. If a run
stops after an upload, check the portal and mail before trying again.
| Piece | Owns | Never does |
|---|---|---|
scripts/sign-all.py (holds the PIN; never commit it) |
the token, the PIN, writing name_Signed.ext |
talk to the portal |
icegate client |
login, plan, challenge, upload, receipts, e-copy | sign, or read the PIN |
scripts/icegate-run.py |
the prompts, code guess, calling the signer and client, draft, Done/ |
touch the PIN or token, bypass a client gate |
%PDF-header and at least one page;- not encrypted;
- a signature field, validated with pyHanko: all signature digests and CMS values must verify and the last signature must cover the entire file;
- file count, per-file size and duplicate names.
Trust is separate from integrity. The PDF validator currently has no trust roots configured, so it cannot establish signer trust.
stage warns if a signature cannot be parsed; submit refuses by default.
A signature that parses but fails validation is rejected by both commands.
Both accept --allow-invalid-signature as an explicit override. A rejected
submission may still leave a record.
The published numbers disagree, so they are taken from the live page rather than from documentation. Everything below was read on 2026-10-28.
| Source | Per file | Per batch | Notes |
|---|---|---|---|
ICEGATE FAQ (/help/faq) |
"lesser than 1 MB" | "max 5 documents" | stale |
| eSANCHIT step-by-step PDF (2022-04) | 1 MB | "maximum 5 documents" | stale |
| Upload dialog, live page text | "Maximum size per file is 2 MB." | "The total number of maximum files are 5 per DRN." | current |
| Upload dialog, live JS | maxSize = parseInt('2097152') = 2 MiB |
upoadSize > 10485760 for a broker account |
per-request cap, not per batch |
Three details that the visible text does not tell you:
- The client-side per-request cap is 10 MiB, despite the 50 MB message. The JS has one branch per user class: 52 428 800 bytes for agency users and 10 485 760 for broker accounts, but both branches print "Maximum of 50 MB is allowed per request." Five 2 MiB files already reach the 10 MiB ceiling, so this is a client-side check; server enforcement has not been established.
- The five-file check does not actually run.
numberOfFileCheck()readsdocumentBean.maxFile, which the server never substitutes, so the comparison is a number against the jQuery function and is always false. The rule is a sentence in the help text, not an enforced client limit. Whether the server enforces it is unknown. - The dialog accepts PDF/Doc/Docx/jpg/jpeg; this client still only sends signed PDFs. Filenames must be unique, which preflight enforces.
Practical numbers used by the client: 2 MiB per file, 5 files per DRN and the
live page's per-request cap. The page is re-read after the handoff, so a change
there is honoured without a release. --strict-limits turns the size and count
warnings into errors.
Everything below was observed against the live portal on 2026-10-28.
| Step | Request |
|---|---|
| Login | POST https://foservices.icegate.gov.in/identity/ext-login |
| Refresh | POST /identity/refresh-token (only before expiry) |
| Dashboard | POST /dashboard/services/get |
| Handoff | POST /esanchit/esanchitDynamicUrl |
| Upload | POST https://www.icegate.gov.in/esanchit/uploadDocs |
| Submit | POST https://www.icegate.gov.in/esanchit/submitDocs |
- The password field is base64 of AES-128-ECB/PKCS7 using the key published in the public bundle. It hides the password from casual inspection in the request body; it is not a secret.
- The dashboard token lasts exactly ten minutes. Activity does not extend it.
Refresh issues a new ten-minute token, but only while the old one is valid;
after expiry the API returns HTTP 409
auth error..!!!. - The handoff ticket is a bearer value in the query string. It must be used immediately and never logged.
www.icegate.gov.insends only its leaf certificate and omits the issuing intermediate. The client appendsEntrust DV TLS Issuing RSA CA 2to thecertifibundle at runtime. Verification stays on.- Redirect chains answer with
http://URLs. Each hop to anicegate.gov.inhost is upgraded to HTTPS before the request is made. uploadDocsreturns the staging page directly. Its rows carry read-onlyDRN/IRN/digiSignValidatedinputs that stay empty untilsubmitDocs, which is also the only action that readsdetailBean[N].docTyp.
A live upload on 2026-10-28 confirmed the shape of the queue:
uploadDocsanswers with one row per file, butDRN,IRN,digiSignValidatedand the document type are read-only inputs that are still empty or unset after the upload.- The row's type widget is a plain
<select name="detailBean[N].docTyp">with noonchangehandler and no save endpoint. ItsNONEoption means "not chosen". - The queue is bound to the eSANCHIT
JSESSIONIDandtabId. A fresh handoff opens an empty page, so rows staged by a previous command are gone.
In short: staging validates the file and parks it in the current session. DRN,
IRN, the signature verdict and the chosen document type are created only by
submitDocs, which icegate esanchit submit posts in the same process.
The portal's document-type options are full strings such as
101000-Registration Document. icegate esanchit doc-types searches the same catalogue
locally. --doc-type / --doc-type-map cross-check the requested codes
against the options the row itself offers, then print them in a
planned type (not saved) column.
Nothing is POSTed for a type. A requested code that the row does not offer is reported as a warning, and the upload is kept.
The portal has no undo, so submission is a state machine rather than a request:
| Step | What happens |
|---|---|
| plan | files are hashed, codes resolved, plan_id = sha256(name,size,sha256,code)[:12] |
| challenge | the user must pass --confirm 'SUBMIT <n> <plan_id>' |
| lock | filelock on <state-dir>/submit.lock; a second process is refused |
| receipt check | <state-dir>/receipts/<plan_id>.json exists means this exact plan already ran |
| upload | the validated file snapshot is checked against the plan, uploaded, and the resulting rows compared to the plan |
| submit | one POST submitDocs carrying tabId and detailBean[N].docTyp |
| dispatch guard | write an unknown receipt before the submit request, so an interrupted process blocks a repeat |
| result | submitted (matching rows, 16-digit DRN and IRNs, and verified signatures) or unknown |
unknown means the request may or may not have been accepted: a timeout, a
5xx, or a 200 without a DRN. The client writes the receipt, prints where it is
and exits 3. It never retries, because a retry can create a second DRN.
Receipts are JSON, one per plan (mode 0600 on POSIX). --allow-duplicate is the only way
to submit the same plan again, and it has to be typed on purpose.
These local protections reduce duplicate risk but cannot guarantee exactly-once
processing. The guard covers only the same plan in the same state directory; it
cannot detect submissions made elsewhere or plans with different names or codes.
Check confirmation mail for existing uploads before submitting.
- Browser-free login worked without an OTP challenge in live tests. Per the user's request, the client has no OTP fallback; rejected logins stop.
- Other submit response shapes and automated reconciliation remain unverified.
- No delete, no beneficiary editing, no CSV tracking.
Each run writes ~/.local/state/icegate-client/runs/<timestamp>-<pid>.jsonl,
mode 0600 on POSIX. It records intent (file names, sizes, SHA-256, document codes),
protocol outcomes (status codes, hop paths, query parameter names), and
staged row identifiers. It never records tokens, session ids, OTPs, passwords,
cookies or handoff URLs with their query strings.
The former top-level stage, submit, handoff, and doc-types commands now live under icegate esanchit; update any scripts that used the old names.