Transita

API and webhooks

Two ways to get your own Transita data into your own tools: a small read-only API, and signed webhooks for the events behind your dashboard. Both work on your own data only.

Dates and scores are estimates from official sources, not legal advice. Transita is not a law firm. Check the official source linked on every item before you act.

Get a key

Sign in, open your profile, and create a key in the Developers card. A key starts with trk_, is shown once, and can only read. You can have 5 at a time and revoke any of them. Send it as a bearer token.

Endpoints

  • GET /api/v1/routesYour tracked routes, each with a status and its next deadline.
  • GET /api/v1/deadlinesUpcoming dates, soonest first. Optional days, 1 to 365, default 90.
  • GET /api/v1/changesRecorded rule and source changes on your routes, newest first. Optional since and route.
curl https://transita.app/api/v1/routes \
  -H "Authorization: Bearer $TRANSITA_API_KEY"
curl "https://transita.app/api/v1/deadlines?days=30" \
  -H "Authorization: Bearer $TRANSITA_API_KEY"
curl "https://transita.app/api/v1/changes?since=2026-09-01" \
  -H "Authorization: Bearer $TRANSITA_API_KEY"

Every response has the same envelope. Dates are ISO dates, and each item carries its source URL and the date we last read it.

{
  "api_version": "v1",
  "as_of": "2026-10-10T08:00:00.000Z",
  "note": "Dates and scores are estimates from official sources, not legal advice.",
  "data": [
    {
      "id": "3f9a1c0e5b7d2a4c8e61b0d2",
      "kind": "expected_decision",
      "title": "Expected decision, UK Skilled Worker",
      "due_on": "2026-11-18",
      "days_left": 39,
      "route_id": "uk-skilled-worker",
      "source": { "url": "https://www.gov.uk/skilled-worker-visa", "as_of": "2026-09-30" },
      "workspace_url": "https://transita.app/dashboard?pathway=uk-skilled-worker&tab=plan"
    }
  ]
}

Errors are JSON with an error.code: 401 for a missing or revoked key, 403 for a missing scope, 400 for a bad parameter, 429 when you go over 120 requests a minute (see the Retry-After header).

Webhooks

Add an https URL in the Developers card and we POST JSON to it when something changes. You can have 3 endpoints. The signing secret is shown once. Use Send test event to check your receiver before you rely on it.

  • deadline.approachingA deadline is coming up. A date on your clock is 14, 7, 1 days away. Sent once per threshold.
  • route.changedA rule changed on a route you track. A rule or source page changed on a route you track. Includes the field names and the official URL.
  • route.score_droppedYour estimated score dropped. Your estimated score fell below a published cutoff or dropped after a rule change.
  • document.expiringA document is about to expire. A document you uploaded is 60, 30, 7 days from expiry, or stops meeting a route rule.

Payloads hold ids, titles, dates and a dated source URL. They never include your documents or anything you typed. Use id to ignore a delivery you have already handled: the same event can arrive more than once.

{
  "id": "evt_5d41402abc4b2a76b9719d91",
  "type": "deadline.approaching",
  "api_version": "v1",
  "created_at": "2026-10-10T08:00:03.000Z",
  "data": {
    "deadline": {
      "id": "3f9a1c0e5b7d2a4c8e61b0d2",
      "kind": "expected_decision",
      "title": "Expected decision, UK Skilled Worker",
      "due_on": "2026-10-17",
      "days_left": 7,
      "route_id": "uk-skilled-worker",
      "source": { "url": "https://www.gov.uk/skilled-worker-visa", "as_of": "2026-09-30" },
      "workspace_url": "https://transita.app/dashboard?pathway=uk-skilled-worker&tab=plan"
    }
  }
}

Verify the signature

Each request has a Transita-Signature header like t=1790000000,v1=5257a8…. The v1 value is an HMAC-SHA256, in hex, of the timestamp, a dot, and the raw request body, keyed with your signing secret. Reject a request whose timestamp is more than 5 minutes from your clock: that is how replays are stopped. Compare in constant time and verify the raw body before you parse it.

const crypto = require("node:crypto");

// rawBody: the request body exactly as received (a string), not re-serialised JSON.
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = header.split(",");
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false; // replay
  const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  return signatures.some(
    (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
  );
}
import hashlib
import hmac
import time

# raw_body: the request body exactly as received (bytes), not re-serialised JSON.
def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = header.split(",")
    t = next((p[2:] for p in parts if p.startswith("t=")), None)
    signatures = [p[3:] for p in parts if p.startswith("v1=")]
    if not t or not signatures:
        return False
    if abs(time.time() - int(t)) > tolerance_seconds:  # replay
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s, expected) for s in signatures)

Delivery and retries

Answer with any 2xx within 8 seconds. Anything else is a failure and is retried after 1 min, 5 min, 30 min, 2 h, 6 h, up to 6 attempts in all, checked every 15 minutes. After 12 failed attempts in a row, or one 410 Gone, we turn the endpoint off and say why in the card. Turn it back on once it works. We do not follow redirects, we only call public https addresses, and we never keep your server's response, only its status code.

Versioning

The path and the api_version field say v1. Within v1 we add fields and event types and never remove or rename one, so ignore fields you do not know. A breaking change ships as v2 beside v1, and v1 keeps working for at least six months after v2 is announced on the changelog.

Questions

Write to hello@transita.app. For AI assistants there is also the MCP server.