{
  "name": "url-unshorten",
  "summary": "Follows a shortened or tracking URL to its real destination and returns every hop it went through.",
  "rationale": "Resolving a short link needs the network, and doing it safely needs a hardened client: an agent that follows the redirects itself will happily walk into a private address or a redirect loop.",
  "family": "fetch",
  "price_usd": "0.002",
  "free": false,
  "timeout_ms": 10000,
  "max_timeout_seconds": 31,
  "input_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "url": {
        "type": "string",
        "maxLength": 2048,
        "format": "uri",
        "description": "The short or tracking URL to follow, up to 2048 characters; every hop, the first included, goes through the SSRF guard."
      },
      "max_hops": {
        "default": 10,
        "description": "Most redirects to follow, 1 to 10, default 10; a chain that has not ended by then fails as unprocessable, and a meta refresh at the limit stops with stopped_reason hop_limit.",
        "type": "integer",
        "minimum": 1,
        "maximum": 10
      },
      "method": {
        "default": "auto",
        "description": "auto sends HEAD and retries the same hop with GET on a 400, 403, 405 or 501; head and get force one method for every hop.",
        "type": "string",
        "enum": [
          "auto",
          "head",
          "get"
        ]
      },
      "follow_meta_refresh": {
        "default": false,
        "description": "Whether to also follow an HTML meta refresh with a delay of 30 seconds or less, found in the first 64 KiB of a 2xx HTML or untyped page; method head turns it off, and JavaScript redirects are never followed.",
        "type": "boolean"
      }
    },
    "required": [
      "url"
    ],
    "additionalProperties": false
  },
  "input_example": {
    "url": "https://grist.tools/samples/web/unshorten-start.html",
    "max_hops": 10,
    "follow_meta_refresh": true
  },
  "output_example": {
    "requested_url": "https://grist.tools/samples/web/unshorten-start.html",
    "source_url": "https://grist.tools/samples/web/unshorten-start.html",
    "final_url": "https://grist.tools/samples/web/unshorten-landing.html",
    "final_status": 200,
    "final_host": "grist.tools",
    "final_content_type": "text/html; charset=utf-8",
    "hop_count": 1,
    "reached_final": true,
    "stopped_reason": null,
    "cross_host": false,
    "hops": [
      {
        "url": "https://grist.tools/samples/web/unshorten-start.html",
        "method": "GET",
        "status": 200,
        "location": "unshorten-landing.html",
        "resolved_location": "https://grist.tools/samples/web/unshorten-landing.html",
        "kind": "meta_refresh",
        "host": "grist.tools"
      },
      {
        "url": "https://grist.tools/samples/web/unshorten-landing.html",
        "method": "GET",
        "status": 200,
        "location": null,
        "resolved_location": null,
        "kind": "final",
        "host": "grist.tools"
      }
    ],
    "initial_host_is_known_shortener": false,
    "shortener_list_version": "2026-08-26",
    "checked_at": "2026-09-01T12:00:00.000Z",
    "fetched_at": "2026-09-01T12:00:00.000Z"
  },
  "errors": [
    "invalid_input",
    "blocked_target",
    "unreachable_target",
    "upstream_timeout",
    "unprocessable",
    "internal"
  ]
}