docs
stormdoor.dev
GATE

/v1/links

Follows a URL's redirect chain and screens where it actually ends up.

METHOD
POST
BODY CAP
4 KiB
CHECKS
3
FINDING CODES
3

POST /v1/links screens one URL. It’s for links a user pasted or clicked through — a “visit my website” field, a link shared in a message you’re relaying, a URL from a form — where what matters is not the URL string itself but where it actually leads once every redirect resolves.

Request#

{ "url": "https://example.com" }
Field Type Required Notes
url string yes 1–2048 characters. Not pre-validated beyond length — an unparseable string simply produces no chain data and no chain-related findings.

There are no optional fields yet.

Request body cap: 4 KB — far more than a single URL needs, with headroom for future optional fields.

Checks this gate runs#

link.redirect_chain and link.destination_reputation make network calls (following redirects, then an RDAP lookup on the final destination); link.shortener is pure computation on the submitted URL string itself.

Check What it does
link.redirect_chain Follows redirects (up to 10 hops), refusing any hop that targets a non-public host — see link.hop_cap_exceeded, link.shortener_chain, link.unsafe_redirect_target.
link.destination_reputation Runs registration-age and typosquat checks against the chain’s final destination domain — see domain.not_registered, domain.newly_registered, domain.typosquat (owned by /domains, reused here against the destination rather than the submitted host).
link.shortener Flags whether the submitted URL itself is on a known shortener host. Contributes data.shortener.service; emits no finding of its own.

This gate does no content scanning of the destination page — no malware signatures, no page-content inspection. Detection here is entirely about the chain’s structure (hop count, shortener services involved, whether any hop was refused) and the final domain’s registration signals.

Example#

curl -s -X POST https://api.stormdoor.dev/v1/links \
  -H "Authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{"url":"https://example.com"}'
{
  "ok": true,
  "verdict": "clean",
  "findings": [],
  "data": {
    "chain": { "hops": [{ "url": "https://example.com", "status": 200 }], "finalUrl": "https://example.com", "truncated": false },
    "destination": { "domain": "example.com", "rdap": { "createdAt": "1995-08-14T04:00:00.000Z", "ageDays": 11367 } }
  },
  "checks_run": ["link.destination_reputation", "link.redirect_chain", "link.shortener"],
  "checks_skipped": [],
  "meta": { "gate": "links", "ruleset": "reference-9f0e0204" }
}

Real response — a direct, single-hop link to a long-registered domain. data.destination is only populated by link.destination_reputation; if that check is skipped for any reason, the field is absent, not null (design rule 7).

See response format for checks_skipped reasons, and don’t proxy that array to your own end users.

Finding codes#

Each code links to its own page with what it looks at, why it matters and what to do.

link.hop_cap_exceededmediumlink.redirect_chainsuspect
link.shortener_chainmediumlink.redirect_chainsuspect
link.unsafe_redirect_targethighlink.redirect_chainblock