Tell your systems the moment it moves

DigiTransito can POST a signed message to an address you control every time a consignment changes state — booked, loaded, arrived, delivered. No polling, no nightly export. Everything you need to write the receiving end is on this page.

1 The consignment moves

Someone marks LR MUM/LR/2847 delivered in the app.

2 We sign the message

HMAC-SHA256 over the timestamp and the raw body, with your endpoint’s own secret.

3 We POST it to you

To the HTTPS address you registered. Redirects are never followed.

4 You answer 2xx

Within ten seconds. Acknowledge first, do the work after.

No 2xx, or no answer inside ten seconds? We come back.

30s 1m 2m 4m 8m

Six attempts on a widening delay — about fifteen minutes end to end, so a deploy or a restart does not lose the event. After that it is marked failed, and you can inspect it and send it again by hand from the Webhooks screen.

Four steps, inside the app

  1. In DigiTransito, open Administration → Webhooks and add one. You give it a name, an https:// address, and tick the events you want.
  2. Copy the signing secret. It is shown once, when the webhook is created, and cannot be looked up afterwards — it is stored encrypted. If it is lost, rotate for a new one; the old one stops working immediately.
  3. Press Verify. We post a challenge to your address and it must send the same value back. Until it does, nothing is delivered.
  4. Switch it on. Deliveries begin.
The verify handshake
# We POST this to your address when you press Verify:
{ "event": "endpoint.verify", "challenge": "9f2c…" }

# Your endpoint must send the SAME challenge value back in its response body.
# Answering with a plain 200 is not enough — that is the whole point of the check.

Two things will stop a webhook working, and both are deliberate. The address must be https — plain http is refused, because the message would be readable in transit. And it must resolve to a public address: one on a private network is refused, and re-checked before every message, so a name that later starts pointing inside a private network stops receiving. We also never follow redirects — point us at the final address, not at something that forwards.

Six events

Deliberately small, and deliberately stable. These names are a contract: we can rename things inside DigiTransito without breaking the code you write against them.

lr.booked A consignment is booked and has its LR number.
lr.in_transit It has been loaded onto a vehicle for the long haul.
lr.arrived It has reached the destination branch and been checked in.
lr.out_for_delivery It is on the delivery run.
lr.delivered Delivery is confirmed, with proof of delivery captured.
lr.cancelled The consignment was cancelled.

The message

{
  "event": "lr.delivered",
  "eventId": "5b1e0c2a-…",
  "occurredAt": "2026-08-30T14:22:10.481Z",
  "data": {
    "lrNo": "LR-2026-004417",
    "status": "delivered",
    "fromStation": "Ahmedabad",
    "toStation": "Nagpur"
  }
}

Order is not guaranteed, and we would rather say so than pretend. Deliveries run in parallel and are retried independently, so a later event can arrive first. Every message carries occurredAt so you can sequence them yourself, and eventId so you can ignore one you have already handled. Store the id; the same message can legitimately arrive twice.

X-DT-Signature sha256= followed by the hex HMAC. This is the one that matters.
X-DT-Timestamp Unix seconds. It is inside the signature, so editing it breaks the check.
X-DT-Delivery-Id Unique per delivery attempt — useful in your own logs.
Content-Type application/json

Checking it is really us

The signature is an HMAC-SHA256 over {timestamp}.{raw body}, keyed with your signing secret. The timestamp is inside the signature on purpose: one that sat outside it would be a freshness check an attacker could simply edit.

Please actually check it. Without this, anyone who learns your address can send you invented deliveries, and your ERP will believe them.

Node.js · Express
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.DT_WEBHOOK_SECRET;

// The raw body is required. Parsing to JSON and re-stringifying it will NOT
// reproduce the same bytes, and the signature will never match.
app.post('/digitransito/events',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-DT-Signature') || '';
    const timestamp = req.get('X-DT-Timestamp') || '';
    const raw = req.body.toString('utf8');

    const expected = 'sha256=' + crypto
      .createHmac('sha256', SECRET)
      .update(timestamp + '.' + raw, 'utf8')
      .digest('hex');

    // timingSafeEqual, not === : a plain comparison leaks how much of the
    // signature was right, one byte at a time.
    const ok = signature.length === expected.length && crypto.timingSafeEqual(
      Buffer.from(signature), Buffer.from(expected),
    );
    if (!ok) return res.status(401).send('bad signature');

    // Optional but recommended: refuse anything older than five minutes, so a
    // captured request cannot be replayed at you later.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(401).send('stale');
    }

    const event = JSON.parse(raw);
    console.log(event.event, event.data.lrNo);

    // Answer 2xx quickly and do the work afterwards. We wait 10 seconds.
    res.sendStatus(200);
  });

app.listen(3000);
Python · Flask
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["DT_WEBHOOK_SECRET"].encode()

@app.post("/digitransito/events")
def events():
    signature = request.headers.get("X-DT-Signature", "")
    timestamp = request.headers.get("X-DT-Timestamp", "")
    raw = request.get_data()          # RAW bytes — not request.json

    expected = "sha256=" + hmac.new(
        SECRET, f"{timestamp}.".encode() + raw, hashlib.sha256,
    ).hexdigest()

    # compare_digest, not == : a plain comparison leaks the signature byte by byte.
    if not hmac.compare_digest(signature, expected):
        abort(401)
    if abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    event = request.get_json()
    print(event["event"], event["data"]["lrNo"])
    return "", 200

What we do if your endpoint is down

  • We retry for about fifteen minutes — six attempts on a widening delay. A deploy or a restart will not lose an event.
  • Anything other than a 2xx is a failure, including a redirect. We do not follow it.
  • Answer within ten seconds. Acknowledge first, do the work afterwards — a slow handler looks identical to a broken one from our side.
  • Failures are visible in the app. The Webhooks list counts them per endpoint, and every delivery can be inspected and sent again by hand — so an address that has quietly stopped working does not need a log to find.

Ready to digitise your fleet?

Create your workspace in under a minute — no credit card, no setup fees. Try everything for 30 days, then keep working on the free plan.

Prefer to talk? Call +91 98981 26295 · Mon–Sat, 10am–7pm IST