A standalone Node.js + Express backend that verifies whether an SMS sender ID belongs to a legitimate registered entity, using the official TRAI SMS Header registry.
It has no frontend and no frontend dependencies — it is a self-contained service that can be run on its own port or mounted inside another Express app, which makes it easy to drop into the SMS scam-detection application later.
data/sms_headers.xlsx ──parser──▶ database/headers.db (SQLite, queried by the API)
└▶ database/headers.json (portable snapshot)
POST /verify-header { "sender": "VK-HDFCBK" }
│
├─ validate the format ────────────▶ 400 if malformed (no DB lookup)
├─ normalise to the header ────────▶ "HDFCBK"
└─ look it up in SQLite ───────────▶ { verified: true, entityName: "HDFC BANK LIMITED" }
The sender ID is validated before any database lookup. Four real-world shapes are accepted:
| # | Format | Examples | Normalises to |
|---|---|---|---|
| 1 | XX-HEADER |
VK-HDFCBK, AX-ICICIB |
HDFCBK, ICICIB |
| 2 | XX-HEADER-TYPE |
JD-HDFCBK-S, JD-HDFCBK-P |
HDFCBK |
| 3 | Bare header | HDFCBK, GODREJHFC, HDFCBK-S |
HDFCBK, GODREJHFC, HDFCBK |
| 4 | Numeric short code | 512, 123456 |
512, 123456 |
Where:
XX— the operator/circle access code: exactly two uppercase letters (VK,AX,VM,JD, …). Stripped before lookup.HEADER— 1–9 uppercase letters and/or digits. TRAI headers are normally 6 characters, but the registry also contains 39 longer entries (AUTOVIT,MERUCAB,GODREJHFC, …) and 1–5 character numeric short codes, so the accepted range is deliberately wider than 6 — a 6-character cap would make those entities impossible to verify.TYPE— one uppercase letter marking the traffic category:Sservice,Ppromotional,Ttransactional,Ggovernment. Stripped before lookup.
Surrounding whitespace is trimmed, so " VK-HDFCBK " is accepted.
| Input | Why |
|---|---|
"", " " |
Empty / whitespace only |
hello, vk-hdfcbk |
Lowercase — sender IDs travel in uppercase |
VK- |
Trailing hyphen, empty header |
-HDFCBK |
Leading hyphen, missing access code |
VK--HDFCBK |
Consecutive hyphens |
VK-HDFC@1, +91VK-HDFCBK |
Illegal characters |
VK HDFCBK, VK_HDFCBK |
Wrong separator |
VKM-HDFCBK, V1-HDFCBK |
Invalid access code |
VK-HDFCBK-SP, VK-HDFCBK-S-P |
Invalid category suffix |
VK-HDFCBKTOOLONG |
Header longer than 9 characters |
missing / non-string sender |
Not a usable sender ID |
A well-formed but unregistered sender ID is not a 400 — it returns 200
with verified: false, because the format was fine and the registry simply had
no match.
A two-group ID like AB-S can be read two ways: access code AB + header S,
or bare header AB + category S. Both readings are tried against the registry
and the one that is actually registered wins.
sms-identity-verification/
├── config/
│ └── index.js # All paths, port and column-name settings
├── data/
│ └── sms_headers.xlsx # Official TRAI SMS header export (input)
├── database/
│ ├── headers.db # SQLite database (generated by the parser)
│ └── headers.json # JSON snapshot (generated by the parser)
├── parser/
│ ├── excelParser.js # Reads + cleans the Excel file
│ └── index.js # CLI runner: Excel -> SQLite + JSON
├── routes/
│ └── verifyRoutes.js # HTTP layer: POST /verify-header, GET /health
├── services/
│ ├── database.js # SQLite connection, schema and queries
│ └── verificationService.js # Header extraction + verification logic
├── test/
│ └── smoke.test.js # End-to-end smoke test (node:test)
├── app.js # Express app (middleware + routes, no listen)
├── server.js # Process entry point (starts the server)
├── index.js # Library entry point for in-process use
└── package.json
Requires Node.js 18 or newer (tested on Node 24).
npm installDependencies (all open source):
| Package | Purpose |
|---|---|
express |
HTTP server and routing |
exceljs |
Reading the .xlsx file |
better-sqlite3 |
Fast synchronous SQLite driver |
cors |
Cross-origin access for API callers |
morgan |
Request logging |
npm run parseThis reads data/sms_headers.xlsx, cleans the data (trims whitespace, skips
empty rows, removes duplicate headers) and rebuilds both outputs from
scratch:
database/headers.dbdatabase/headers.json
Example output:
[parser] Reading .../data/sms_headers.xlsx ...
[parser] Sheet "List of Headers": 23192 data rows, 0 empty/incomplete skipped, 2 duplicates removed, 23190 unique headers kept.
[parser] Wrote 23190 rows to .../database/headers.db
[parser] Wrote 23190 records to .../database/headers.json
[parser] Done in 673 ms.
Replace data/sms_headers.xlsx with a newer TRAI export and run npm run parse
again — nothing else needs to change. The parser matches columns by title
(Header, Principal Entity Name) rather than by position, so a reordered
export still works. Extra accepted column titles are listed in
config/index.js.
npm start[server] SMS Identity Verification API listening on http://localhost:3000
[server] Registry loaded: 23190 registered TRAI headers.
Use npm run dev for auto-restart on file changes. The port can be changed with
the PORT environment variable.
Request:
{ "sender": "VK-HDFCBK" }Success (200):
{
"verified": true,
"header": "HDFCBK",
"entityName": "HDFC BANK LIMITED",
"message": "Registered TRAI SMS Header"
}Not registered (200):
{
"verified": false,
"header": "FAKEBK",
"entityName": null,
"message": "Header not found in TRAI registry"
}Malformed sender ID (400) — see
Supported sender ID formats. The registry is
not queried in this case:
{
"verified": false,
"header": null,
"entityName": null,
"message": "Invalid sender ID format. Expected XX-HEADER, XX-HEADER-TYPE, a bare header, or a numeric short code"
}Registry not built (503) — run npm run parse:
{
"verified": false,
"header": null,
"entityName": null,
"message": "SQLite database not found at ... Run \"npm run parse\" to build it from the Excel file."
}{
"status": "ok",
"registry": {
"records": 23190,
"source_file": "sms_headers.xlsx",
"sheet_name": "List of Headers",
"record_count": "23190",
"parsed_at": "2026-08-07T17:14:22.780Z"
}
}-
Start the server:
npm start. -
Create a new request:
- Method:
POST - URL:
http://localhost:3000/verify-header
- Method:
-
Open the Body tab → select raw → choose JSON from the dropdown. (Postman then sets
Content-Type: application/jsonautomatically; if you set headers manually, add it yourself — the request is rejected without it.) -
Paste the body:
{ "sender": "VK-HDFCBK" } -
Click Send. You should get the success response shown above.
Try these senders to exercise the different paths:
| Sender | Expected |
|---|---|
VK-HDFCBK |
200 verified: true, HDFC BANK LIMITED |
AX-ICICIB |
200 verified: true, ICICI BANK LIMITED |
VM-SBIBNK |
200 verified: true, STATE BANK OF INDIA |
JD-HDFCBK-S |
200 verified: true (category suffix stripped) |
HDFCBK |
200 verified: true (bare header) |
512 |
200 verified: true, KARODIAL (short code) |
123456 |
200 verified: true, Blue Jay Finlease Limited |
VK-FAKEBK |
200 verified: false, header not in registry |
hello |
400 malformed sender ID |
VK- |
400 malformed sender ID |
-HDFCBK |
400 malformed sender ID |
VK-HDFC@1 |
400 malformed sender ID |
VK--HDFCBK |
400 malformed sender ID |
| (no body) | 400 malformed sender ID |
curl -X POST http://localhost:3000/verify-header -H "Content-Type: application/json" -d "{\"sender\":\"VK-HDFCBK\"}"npm testTwo suites:
test/senderValidation.test.js— unit tests for format validation and header normalisation across every supported and rejected shape. Runs without the database.test/smoke.test.js— end-to-endPOST /verify-headerround trips on a random port. Requires the parser to have been run first.
The module is frontend-agnostic and can be consumed three ways:
1. As a separate service — run it on its own port and call
POST /verify-header over HTTP.
2. Mounted into an existing Express app:
const identityApp = require('./sms-identity-verification/app');
hostApp.use('/identity', identityApp); // -> POST /identity/verify-header3. Called directly in-process (no HTTP):
const { verifySender } = require('./sms-identity-verification');
const result = verifySender('VK-HDFCBK');
// { verified: true, header: 'HDFCBK', entityName: 'HDFC BANK LIMITED',
// message: 'Registered TRAI SMS Header', validFormat: true }verifySender() returns an extra validFormat flag that the HTTP layer uses to
choose 400 vs 200 and strips from the response body; in-process callers can use
it to tell "malformed sender ID" apart from "not in the registry".
isValidSenderId(), extractHeader() and runParser() are exported too, so the
host app can validate or normalise a sender ID on its own, or refresh the
registry programmatically instead of shelling out to the CLI.
- Sender ID validation is case-sensitive — lowercase input is treated as
malformed, since sender IDs are transmitted in uppercase. The registry
lookup itself is case-insensitive (the
headercolumn usesCOLLATE NOCASE). To accept lowercase, uppercase the value innormaliseSender()inservices/verificationService.js. - One registry entry (
HP POL) contains a space and therefore cannot be matched, because spaces are not legal in a sender ID. database/headers.dbanddatabase/headers.jsonare generated artefacts and are git-ignored — runnpm run parseafter cloning.- Verification confirms that a header is registered with TRAI; it does not by itself prove a message is safe. It is one signal for the scam-detection pipeline, not a verdict.