GET/api/v2/redirects
Redirect chain API
Follow a URL's redirects hop by hop (HTTP 3xx, meta refresh and Refresh headers) and judge the chain the way Google and browsers do.
- 20 calls a minute per IP (IPv6 per /56)
- Same engine as the Redirect Checker tool
{
"query": {
"url": "http://github.com/",
"ua": "chrome"
},
"verdict": "ok",
"reasons": [
"ok"
],
"end": "final",
"finalUrl": "https://github.com/",
"finalStatus": 200,
"redirects": 1,
"httpRedirects": 1,
"clientRedirects": 0,
"totalMs": 178,
"hops": [
{
"n": 0,
"url": "http://github.com/",
"status": 301,
"state": "ok",
"error": null,
"kind": "http",
"arrivedBy": "start",
"redirectType": "permanent",
"location": "https://github.com/",
"next": "https://github.com/",
"nextRefused": null,
"delay": null,
"ip": "20.207.73.82",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 38,
"dnsMs": 1,
"connectMs": 18,
"tlsMs": null,
"ttfbMs": 19,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 0
},
{
"n": 1,
"url": "https://github.com/",
"status": 200,
"state": "ok",
"error": null,
"kind": "final",
"arrivedBy": "http",
"redirectType": null,
"location": null,
"next": null,
"nextRefused": null,
"delay": null,
"ip": "20.207.73.82",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 140,
"dnsMs": 0,
"connectMs": 18,
"tlsMs": 20,
"ttfbMs": 38,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 3
}
],
"finalPage": {
"url": "https://github.com/",
"title": "GitHub · Change is constant. GitHub keeps you ahead. · GitHub · GitHub",
"canonical": "https://github.com/",
"canonicalSelf": true,
"noindex": false,
"nofollow": false,
"jsRedirect": null
},
"issues": [],
"tookMs": 674
}How it works
Asks the URL from our server and follows every step the way a browser does: HTTP 301/302/303/307/308, <meta http-equiv=refresh> and the Refresh header, carrying cookies, up to 20 hops. Each hop has its status, Location, timing (DNS, connect, TLS, first byte) and what kind of redirect Google reads it as.
The verdict names the worst thing about the chain: a loop, too many hops, an https → http downgrade, a temporary redirect where a permanent one belongs, more than 3 hops (Google's advice), more than 10 (Googlebot stops), an end on a 4xx/5xx, or a client-side step. JavaScript redirects are spotted in inline scripts but never run or followed.
Honest limits: a hop that times out or a name our DNS couldn't resolve is never called broken: our own failures make the answer incomplete. A bot wall (Cloudflare challenge, 403, 429) is blocked, not broken. Private addresses in the chain are not followed (nextRefused: private).
Answers kept: The same run serves both endpoints and the HTTP Header Checker and Redirect Checker pages: a finished check is kept 2 minutes (30 seconds when a part ran out of time) per URL and user agent. A run that ended on a bot wall or whose first URL got no answer isn't kept.
Query parameters
urlstringrequiredA web address: a full URL (https://github.com/about) or a bare name (github.com, checked as https://). Private and reserved addresses are refused.uastringoptionaldefaultchromeThe user agent to send:chrome(Chrome on Windows, the default),iphone(Safari on iOS),googlebot,bingbotordnsrobot(ours). Sites answer bots and phones differently.one of
chrome,iphone,googlebot,bingbot,dnsrobot
Response fields
200 with this JSON object, plus tookMs, and incomplete with warnings when a source failed. Fields marked optional appear only when they apply.
queryobjectalwaysWhat was looked up, normalised (lower case, no trailing dot, IDNs as Punycode).urlstringalwaysThe URL checked: scheme added when missing (https), host lower case and Punycode, default port and #fragment dropped.uastringalwaysThe user agent the check sent.one of
"chrome","iphone","googlebot","bingbot","dnsrobot"
verdictstringalwaysThe chain in one word, most serious first:direct(no redirect),ok(permanent redirects only, ≤ 3, ends 2xx),temporary,long(> 3),googlebotStops(> 10),loop,downgrade(https → http),broken(ends 4xx/5xx or a dead hop),blocked(a bot wall),client(a meta refresh or JavaScript step),unreachable(the first URL got no answer),tooMany(> 20),notFollowed(our limits) orincomplete(our deadline or lookup).one of
"direct","ok","temporary","long","googlebotStops","loop","downgrade","broken","blocked","client","unreachable","tooMany","notFollowed","incomplete"reasonsarray of stringsalwaysEvery verdict that applies, most serious first (the first isverdict).endstringalwaysWhy the chain ended.one of
"final","loop","tooMany","noLocation","choice","refused","tooLong","limited","failed","skipped"finalUrlstring or nullalwaysThe URL the chain ends on with an answer; null when the last hop got none.finalStatusinteger or nullalwaysThat answer's status.redirectsintegeralwaysRedirects followed (HTTP, meta refresh and Refresh header).httpRedirectsintegeralwaysOf which HTTP 3xx.clientRedirectsintegeralwaysOf which meta refresh or Refresh header.totalMsintegeralwaysThe hops' times added up.hopsarray of objectsalwaysEvery request, in order (at most 21).Show 25 fields
nintegeralwaysPosition in the chain, from 0 (issue messages count hops from 1).urlstringalwaysThe URL asked.statusinteger or nullalwaysThe HTTP status, or null when no answer came (seestate).statestringalwaysokwhen an HTTP answer came (any status); otherwise why not.dnsfail,headTooLargeandskippedare on our side, never the site's.one of
"ok","dns","dnsfail","refused","timeout","tls","reset","private","headTooLarge","skipped"errorstring or nullalwaysA short code for a failed hop (ENOTFOUND,ECONNREFUSED,ETIMEDOUT,ETLS…).kindstringalwaysWhat this hop does next: anhttpredirect, ametarefresh, arefreshheader, orfinal(the chain ends here).one of
"http","meta","refresh","final"arrivedBystringalwaysHow the chain got here.one of
"start","http","meta","refresh"redirectTypestring or nullalwaysGoogle's reading: 301/308 and an instant meta refresh are permanent, 302/303/307 and a delayed refresh temporary.one of
"permanent","temporary",nulllocationstring or nullalwaysThe Location header as sent (on a 3xx).nextstring or nullalwaysWhere this hop sends the visitor (absolute URL).nextRefusedstring or nullalwaysThe next URL exists but wasn't asked: a private address, not http(s), port 25, our own API, over 2,000 characters, or that site's per-minute limit ran out.one of
"private","invalid","port","own","tooLong","limited",nulldelayinteger or nullalwaysSeconds before a meta refresh or Refresh header fires.ipstring or nullalwaysThe address we connected to.httpVersionstring or nullalwaysFrom the status line (1.1); null when read through Cloudflare's network (via: fetch).viastringalwayssocket: our own connection, headers exactly as sent;fetch: through Cloudflare's network (sites on Cloudflare and IPv6-only names), header names lower-cased.one of
"socket","fetch"timeMsintegeralwaysThis hop's total time from our server, our DNS lookup included.dnsMsinteger or nullalwaysOur DNS lookup (0 when the chain already knew the name).connectMsinteger or nullalwaysTCP connect (absent throughfetch).tlsMsinteger or nullalwaysTLS handshake after the connect.ttfbMsinteger or nullalwaysRequest sent to first byte.crossSitebooleanalwaysThe next URL is on another registrable site.downgradebooleanalwaysThis step goes from https:// to http://.loopbooleanalwaysThe next URL was already asked with the same cookies (browsers stop: ERR_TOO_MANY_REDIRECTS).wallstring or nullalwaysA bot wall or refusal of our check (cloudflare,akamai,captcha,forbidden,rate…): the site is up.cookiesintegeralwaysSet-Cookie headers on this answer.
finalPageobject or nullalwaysWhat the final HTML says about itself (null when the last answer isn't HTML or wasn't read).Show 7 fields
urlstringalwaysThe page's URL.titlestring or nullalwaysThe <title>.canonicalstring or nullalways<link rel=canonical>, resolved.canonicalSelfboolean or nullalwaysThe canonical names the URL the chain ended on; null when there is none.noindexbooleanalwaysThe page or its X-Robots-Tag says noindex.nofollowbooleanalwaysThe page or its X-Robots-Tag says nofollow.jsRedirectobject or nullalwaysA likely JavaScript redirect in an inline script (never run, never followed).Show 2 fields
urlstringalwaysIts target.viastringalwaysWhat it assigns to, e.g.location.href.
issuesarray of objectsalwaysProblems found in the target's configuration, worst first. Empty when there are none.severitystringalwayserrorbreaks something,warningrisks it,infois worth knowing.one of
"error","warning","info"codestringalwaysStable machine-readable code.messagestringalwaysThe problem and what to do about it, in plain English.
Errors
422target_not_allowedThe URL points at a private or reserved address, port 25, or our own API.429rate_limitedOver your 20 a minute, or the target site's 40 a minute from everyone (the message says which).503unavailableThe checker's rate limiter isn't reachable: it fails closed rather than run a check uncounted.
Plus the errors every endpoint can return: 400 for a missing or unreadable parameter, 429 past the limit, 503 when every source failed.
Code samples
curl -G "https://dnsrobot.net/api/v2/redirects" \
--data-urlencode "url=http://github.com"const params = new URLSearchParams({ url: "http://github.com" });
const res = await fetch(`https://dnsrobot.net/api/v2/redirects?${params}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data.hops);import requests
r = requests.get(
"https://dnsrobot.net/api/v2/redirects",
params={"url": "http://github.com"},
timeout=30,
)
data = r.json()
if not r.ok:
raise RuntimeError(data["error"]["message"])
print(data["hops"])package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
q := url.Values{"url": {"http://github.com"}}
res, err := http.Get("https://dnsrobot.net/api/v2/redirects?" + q.Encode())
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(data["error"])
}
fmt.Println(data["hops"])
}<?php
$query = http_build_query(["url" => "http://github.com"]);
$context = stream_context_create(["http" => ["ignore_errors" => true, "timeout" => 30]]);
$data = json_decode(file_get_contents("https://dnsrobot.net/api/v2/redirects?" . $query, false, $context), true);
if (isset($data["error"])) {
throw new RuntimeException($data["error"]["message"]);
}
print_r($data["hops"]);require "net/http"
require "json"
uri = URI("https://dnsrobot.net/api/v2/redirects")
uri.query = URI.encode_www_form(url: "http://github.com")
data = JSON.parse(Net::HTTP.get(uri))
raise data["error"]["message"] if data["error"]
puts data["hops"]// Cargo.toml: reqwest = { version = "0.13", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
let res = reqwest::blocking::get("https://dnsrobot.net/api/v2/redirects?url=http%3A%2F%2Fgithub.com")?;
let ok = res.status().is_success();
let data: serde_json::Value = res.json()?;
if !ok {
return Err(data["error"]["message"].to_string().into());
}
println!("{}", data["hops"]);
Ok(())
}using System.Text.Json;
using var http = new HttpClient();
var res = await http.GetAsync("https://dnsrobot.net/api/v2/redirects?url=http%3A%2F%2Fgithub.com");
var data = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!res.IsSuccessStatusCode)
throw new Exception(data.GetProperty("error").GetProperty("message").GetString());
Console.WriteLine(data.GetProperty("hops"));http://github.com: one permanent redirect to HTTPS
GET /api/v2/redirects?url=http%3A%2F%2Fgithub.com{
"query": {
"url": "http://github.com/",
"ua": "chrome"
},
"verdict": "ok",
"reasons": [
"ok"
],
"end": "final",
"finalUrl": "https://github.com/",
"finalStatus": 200,
"redirects": 1,
"httpRedirects": 1,
"clientRedirects": 0,
"totalMs": 178,
"hops": [
{
"n": 0,
"url": "http://github.com/",
"status": 301,
"state": "ok",
"error": null,
"kind": "http",
"arrivedBy": "start",
"redirectType": "permanent",
"location": "https://github.com/",
"next": "https://github.com/",
"nextRefused": null,
"delay": null,
"ip": "20.207.73.82",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 38,
"dnsMs": 1,
"connectMs": 18,
"tlsMs": null,
"ttfbMs": 19,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 0
},
{
"n": 1,
"url": "https://github.com/",
"status": 200,
"state": "ok",
"error": null,
"kind": "final",
"arrivedBy": "http",
"redirectType": null,
"location": null,
"next": null,
"nextRefused": null,
"delay": null,
"ip": "20.207.73.82",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 140,
"dnsMs": 0,
"connectMs": 18,
"tlsMs": 20,
"ttfbMs": 38,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 3
}
],
"finalPage": {
"url": "https://github.com/",
"title": "GitHub · Change is constant. GitHub keeps you ahead. · GitHub · GitHub",
"canonical": "https://github.com/",
"canonicalSelf": true,
"noindex": false,
"nofollow": false,
"jsRedirect": null
},
"issues": [],
"tookMs": 674
}http://google.com: two hops to www
GET /api/v2/redirects?url=http%3A%2F%2Fgoogle.com{
"query": {
"url": "http://google.com/",
"ua": "chrome"
},
"verdict": "temporary",
"reasons": [
"temporary"
],
"end": "final",
"finalUrl": "https://www.google.com/?gws_rd=ssl",
"finalStatus": 200,
"redirects": 2,
"httpRedirects": 2,
"clientRedirects": 0,
"totalMs": 1413,
"hops": [
{
"n": 0,
"url": "http://google.com/",
"status": 301,
"state": "ok",
"error": null,
"kind": "http",
"arrivedBy": "start",
"redirectType": "permanent",
"location": "http://www.google.com/",
"next": "http://www.google.com/",
"nextRefused": null,
"delay": null,
"ip": "142.251.221.174",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 381,
"dnsMs": 1,
"connectMs": 38,
"tlsMs": null,
"ttfbMs": 342,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 0
},
{
"n": 1,
"url": "http://www.google.com/",
"status": 302,
"state": "ok",
"error": null,
"kind": "http",
"arrivedBy": "http",
"redirectType": "temporary",
"location": "https://www.google.com/?gws_rd=ssl",
"next": "https://www.google.com/?gws_rd=ssl",
"nextRefused": null,
"delay": null,
"ip": "142.251.156.119",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 376,
"dnsMs": 0,
"connectMs": 16,
"tlsMs": null,
"ttfbMs": 360,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 3
},
{
"n": 2,
"url": "https://www.google.com/?gws_rd=ssl",
"status": 200,
"state": "ok",
"error": null,
"kind": "final",
"arrivedBy": "http",
"redirectType": null,
"location": null,
"next": null,
"nextRefused": null,
"delay": null,
"ip": "142.251.156.119",
"httpVersion": "1.1",
"via": "socket",
"timeMs": 656,
"dnsMs": 0,
"connectMs": 21,
"tlsMs": 108,
"ttfbMs": 267,
"crossSite": false,
"downgrade": false,
"loop": false,
"wall": null,
"cookies": 6
}
],
"finalPage": {
"url": "https://www.google.com/?gws_rd=ssl",
"title": "Google",
"canonical": null,
"canonicalSelf": null,
"noindex": false,
"nofollow": false,
"jsRedirect": null
},
"issues": [
{
"severity": "warning",
"code": "temporary_redirect",
"message": "Hop 2 is a temporary redirect (302): Google may keep the old URL. Use 301 or 308 for a permanent…"
}
],
"tookMs": 5051
}Shortened for the page: long lists show their first items.