<!-- Source: https://issued.live/developers/api -->
issued.live
 /
 Public API

# Public API

One GET returns the certificate, issuer, expiry, hosting address and
 registration dates for any domain. No key, no sign-up, 1,000 requests a day.

This page documents the three endpoints that answer without a credential: the domain
 record, corpus statistics and the live certificate feed. It is for anyone checking
 certificate expiry, enriching a list of domains, or watching what appears in the
 Certificate Transparency logs.

A Basic subscription uses these same endpoints with a bearer key that lifts the daily
 cap. The [developer hub](/developers) maps out which plan reaches what.

- [Quickstart](#quickstart)
- [Authentication](#auth)
- [Endpoint reference](#endpoints)
- [GET /{domain}](#domain)
- [GET /api/stats](#stats)
- [GET /api/feed](#feed)
- [Response fields](#fields)
- [Content negotiation](#negotiation)
- [Code examples](#examples)
- [Errors](#errors)
- [Rate limits](#limits)
- [FAQ](#faq)
- [Where to go next](#next)

## Quickstart: one request, no key

Put a domain after the hostname. That is the whole API.

```
$ curl https://issued.live/github.com
```

```
{
  "domain":          "github.com",
  "registered":      "2007-10-09T18:20:50Z",
  "expires":         "2026-10-09T18:20:50Z",
  "ssl_expires":     "2026-12-26T23:59:59Z",
  "ssl_cert":        "9F53ECDDF913AFBA5678C924A95149D2",
  "ssl_issuer":      "Amazon",
  "ip":              "140.82.113.4",
  "last_update":     "2026-09-14T08:07:30Z",
  "first_cert_seen": null,
  "tracked":         false,
  "first_zone_seen": null
}
```

The same URL serves a readable page to a browser and JSON to everything else. curl gets
 JSON because of its `User-Agent`. To ask for JSON explicitly from any client:

```
$ curl -H "Accept: application/json" https://issued.live/github.com
```

Every timestamp is RFC 3339 in UTC. Keys arrive in the order shown above,
 and that order is a published contract: new fields are appended, existing ones stay put.

## Authentication: free needs no key, Basic sends a bearer token

The free tier sends no credential at all. Send a request and read the answer.

Basic subscribers send their key as a bearer token. The key raises the allowance, and
 the response body stays byte-identical.

```
$ curl -sS https://issued.live/github.com \
    -H "Accept: application/json" \
    -H "Authorization: Bearer YOUR_KEY"
```

| Plan | Credential | Allowance |
|---|---|---|
| Free | none | 1,000 requests a day per client address, resetting at 00:00 UTC. |
| Basic | `Authorization: Bearer <key>` | 60 requests a minute, with no daily cap. |
| Plus | `Authorization: Bearer <key>` | 300 requests a minute, plus reverse IP, the certificate and key pivots, the extended record and the timeline |
| Pro | `Authorization: Bearer <key>` | 1,200 requests a minute, plus the keyed endpoints under `/api/v1/`. |

**Send the key in the header, and only in the header.** Query strings land
 in access logs, proxy logs and `Referer` headers, so a key in a URL leaks to
 every hop it passes through. There is no `?key=` parameter, and adding one
 would authenticate nothing.

Subscribers [sign in](/login) to create and rotate a key on the account key
 page. [Pricing](/pricing) lists what each plan costs and what it reaches.

Store the key in an environment variable or a secret store. Every code
 example below that needs one reads it from the environment for that reason.

## Endpoint reference

Three endpoints answer without a key. Each takes GET, and each returns JSON to a
 non-browser client.

### GET /{domain} returns one domain record

GET https://issued.live/{domain}

Returns the current certificate, issuer, expiry, hosting address and registration dates
 for a registrable domain. Subdomains are accepted and resolved to their registrable
 domain, so `www.github.com` and `github.com` return the same
 record.

| Parameter | Values | Meaning |
|---|---|---|
| `format` | `json`, `html` | Query parameter. Overrides the `Accept` header and the `User-Agent`, so a browser can be handed JSON and a script can be handed the page. |
| `.json` | path suffix | Forces JSON. `/github.com.json` answers JSON to any client, including a browser. |
| `.md` | path suffix | Returns the record as markdown, with the address history and DNS records the JSON contract omits. See [content negotiation](#negotiation). |

The path itself is normalized. Mixed case, a trailing slash and a trailing
 `/index.html` each answer `301`. The `Location` header
 names the canonical lowercase path, so one domain has exactly one URL.

```
$ curl -sS -H "Accept: application/json" https://issued.live/github.com
```

```
{
  "domain": "github.com",
  "registered": "2007-10-09T18:20:50Z",
  "expires": "2026-10-09T18:20:50Z",
  "ssl_expires": "2026-12-26T23:59:59Z",
  "ssl_cert": "9F53ECDDF913AFBA5678C924A95149D2",
  "ssl_issuer": "Amazon",
  "ip": "140.82.113.4",
  "last_update": "2026-09-14T08:07:30Z",
  "first_cert_seen": null,
  "tracked": false,
  "first_zone_seen": null
}
```

A record with nulls in it is the normal case. Each null says the same thing: we hold no
 value for that field.

In the record below we hold no registry data, so `registered` and
 `expires` are null, while the certificate and zone file sightings are
 known.

```
$ curl -sS -H "Accept: application/json" https://issued.live/loop-lumen.com
```

```
{
  "domain": "loop-lumen.com",
  "registered": null,
  "expires": null,
  "ssl_expires": "2026-12-02T14:34:34Z",
  "ssl_cert": "C7D6BDDD6562183C1111BE68F4209043",
  "ssl_issuer": "Let's Encrypt",
  "ip": "172.64.80.1",
  "last_update": "2026-09-09T07:16:57Z",
  "first_cert_seen": "2026-09-01T11:04:26Z",
  "tracked": false,
  "first_zone_seen": "2026-09-02T06:03:27Z"
}
```

| Status | Body | When |
|---|---|---|
| `200` | the record | We hold a record for the name. |
| `301` | empty | The path is not canonical. Follow `Location`. |
| `404` | `{"error":"domain not found"}` | We hold no record for the name, or the name fails validation. The message reads `domain not found or invalid` in the second case. |
| `503` | `{"error":"timeout"}` | The lookup ran past its 15-second deadline. Retry once. |

### GET /api/stats returns corpus-wide counts

GET https://issued.live/api/stats

Domains known, certificates seen in the last 24 hours, ingest rate, top issuers, a
 per-TLD breakdown, log health and storage headroom. No key required, and no parameters.

A generator rebuilds the snapshot every ten minutes and the endpoint serves that copy,
 so polling it is cheap. Read `as_of` to see how old the numbers are.

```
$ curl -sS https://issued.live/api/stats | jq '.totals'
```

The full response is shown below. `ingest` carries 97 quarter-hour points and
 `tld_table` one row per TLD, about 1,600 of them; both are cut to a single
 entry here so the shape stays readable.

```
{
  "as_of": "2026-09-14T14:42:44Z",
  "totals": {
    "domains": 422778156,
    "certs_24h": 17097266,
    "names_24h": 12713928,
    "entries_24h": 227597036
  },
  "ingest": [
    { "t": "2026-09-13T14:45:00Z", "entries": 2151283, "certs": 2134979 }
  ],
  "issuers": [
    { "name": "Let's Encrypt", "n": 30240917 },
    { "name": "Google Trust Services", "n": 16262052 },
    { "name": "Amazon", "n": 13710640 }
  ],
  "tlds": [
    { "name": "com", "n": 187634929 },
    { "name": "dev", "n": 24688635 },
    { "name": "net", "n": 15812094 }
  ],
  "logs": { "total": 69, "erroring": 2, "behind": 715979439 },
  "storage": {
    "active_bytes": 484458776567,
    "free_bytes": 354349703168,
    "days_to_full": 28.43123501593554
  },
  "dns": {
    "domains": 422778156,
    "ipv4": 318935545,
    "ipv6": 100280024,
    "no_dns": 102609024,
    "v4_addrs": 31270310,
    "v6_addrs": 154872551
  },
  "tld_table": [
    { "tld": "com", "domains": 187634929, "no_dns": 33974871,
      "ipv4": 153452308, "ipv6": 37850274,
      "v4_addrs": 13983764, "v6_addrs": 85803843 }
  ],
  "api_example": {
    "domain": "tuxxin.com",
    "registered": "2011-04-27T06:24:46Z",
    "expires": "2027-04-27T06:24:46Z",
    "ssl_expires": "2026-11-28T10:58:19Z",
    "ssl_cert": "431095AFAB96BC5C282ED931FC907068",
    "ssl_issuer": "Google Trust Services",
    "ip": "104.21.65.210",
    "last_update": "2026-09-06T21:35:53Z",
    "first_cert_seen": null,
    "tracked": false,
    "first_zone_seen": null
  },
  "api_example_domain": "tuxxin.com"
}
```

| Field | Type | Meaning |
|---|---|---|
| `as_of` | string | When the snapshot was built. Anything up to ten minutes old. |
| `totals.domains` | number | Registrable domains we hold a record for. |
| `totals.certs_24h` | number | Certificates observed in the last 24 hours. |
| `totals.names_24h` | number | Distinct names carried by those certificates. |
| `totals.entries_24h` | number | Log entries read in the last 24 hours, precertificates included. |
| `ingest[]` | array | Quarter-hour points of `entries` and `certs`, each stamped `t`. |
| `issuers[]` | array | Top issuing CAs as `name` and count `n`. |
| `tlds[]` | array | Top TLDs by domain count, same `name` and `n` shape. |
| `logs.total` | number | Certificate Transparency logs we read. |
| `logs.erroring` | number | Of those, how many are failing right now. |
| `logs.behind` | number | Entries still to read across every log. |
| `storage.active_bytes` | number | Bytes the corpus occupies on disk. |
| `storage.free_bytes` | number | Bytes left on the data volume. |
| `storage.days_to_full` | number | Days of headroom at the current growth rate. |
| `dns.domains` | number | All registrable domains known. The four fields below describe this population. |
| `dns.ipv4` | number | Of those, how many have at least one A record. |
| `dns.ipv6` | number | Of those, how many have at least one AAAA record. |
| `dns.no_dns` | number | Of those, how many have never resolved to any address. |
| `dns.v4_addrs` | number | Distinct IPv4 addresses observed. This counts addresses, so it cannot be compared with the domain counts above. |
| `dns.v6_addrs` | number | Distinct IPv6 addresses observed. |
| `tld_table[]` | array | One row per TLD, carrying `tld`, `domains`, `no_dns`, `ipv4`, `ipv6`, `v4_addrs` and `v6_addrs`. |
| `api_example` | object | A verbatim reply from the domain endpoint, used by the home page. Same shape as a lookup. |
| `api_example_domain` | string | The domain in that example. |

### GET /api/feed returns certificates from the last 30 minutes

GET https://issued.live/api/feed

A rolling 30-minute window of certificates, newest first. No key required.

| Parameter | Default | Meaning |
|---|---|---|
| `limit` | 50 | Rows to return, capped at 500. A value above the cap is clamped to 500, and a value of zero or below falls back to 50. An unparseable value also falls back to 50. |

```
$ curl -sS "https://issued.live/api/feed?limit=3"
```

```
{
  "certificates": [
    {
      "seen_at": "2026-09-14T14:47:49Z",
      "issuer": "Amazon",
      "name": "test-5ffe5fda.osu.prod.canaries.quickbeam.acm.aws.dev",
      "name_count": 1
    },
    {
      "seen_at": "2026-09-14T14:47:49Z",
      "issuer": "GoDaddy.com",
      "name": "toroasters.ca",
      "name_count": 1
    },
    {
      "seen_at": "2026-09-14T14:47:49Z",
      "issuer": "Amazon",
      "name": "test-d0be7068.syd.prod.canaries.quickbeam.acm.aws.dev",
      "name_count": 1
    }
  ],
  "count": 3,
  "window": "30m"
}
```

| Field | Type | Meaning |
|---|---|---|
| `window` | string | The span the feed covers. Always `30m`. |
| `count` | number | Rows in `certificates`. |
| `certificates[].seen_at` | string | When we read the log entry carrying this certificate. |
| `certificates[].issuer` | string | Issuing CA organization. |
| `certificates[].name` | string | One name from the certificate. |
| `certificates[].name_count` | number | How many names the certificate carries in total. |

**This endpoint serves a window and has no cursor.** Poll it and you see
 what is current. Between two polls you may miss certificates that arrived and aged
 out.

For gap-free delivery, use the cursor-paged registration feed described on
 [data feeds and downloads](/developers/feeds).

## Response fields of a domain record

| Field | Type | Meaning |
|---|---|---|
| `domain` | string | The registrable domain the record describes. |
| `registered` | string \| null | The earliest creation date in the registry (WHOIS and RDAP) data we hold. **null when we hold none**, which can happen while `expires` is known. We never guess it or derive it from certificate dates or zone files. |
| `expires` | string \| null | Registration expiry, where we hold one. Same null semantics. |
| `ssl_expires` | string \| null | Not-after date of the most recently observed certificate. |
| `ssl_cert` | string \| null | Certificate identifier: 32 uppercase hex characters, stable per certificate. This is our own identifier, so a SHA-256 of the certificate will not match it. Pass it to the Pro endpoint `/api/v1/cert/{cert_id}` to fetch the certificate and every name issued alongside it. |
| `ssl_issuer` | string \| null | Issuing CA organization, as it appears in the certificate. |
| `ip` | string \| null | An address from the most recent new or changed A or AAAA answer our resolvers recorded for the domain or a name under it. For a very small number of domains we have no answer of our own, and the address comes from a third-party hosting dataset dated 2026-08-05. |
| `last_update` | string | The newest timestamp we hold for the domain from any source: a DNS answer, a certificate, a zone file or a registry record. For registry data it is when the record was fetched, which can predate our own collection. **Use this to judge freshness.** |
| `first_cert_seen` | string \| null | The earliest time our Certificate Transparency collection recorded a certificate for this name. It is not when the name's first certificate was issued: collection began in August 2026. For many names our records from before 2026-09-05 were lost to a loading error, so the value can be later than our first sighting, or null while `ssl_cert` is known. |
| `tracked` | boolean | Whether the domain is on our active monitoring list. |
| `first_zone_seen` | string \| null | When the domain first appeared as a new name in a registry zone file we hold. Our zone history begins in March 2025, so a domain already listed then and never deleted has none. It is an upper bound on the domain's first registration. For a domain that was deleted and registered again it can predate the current registration by months. |

All timestamps are RFC 3339 in UTC. Keys are returned in the order shown above.

A null is an answer about our holdings: it says we hold no value for that field. Treat
 it as unknown, and read `last_update` to see how recent our newest information
 about the domain is.

The JSON body carries exactly these eleven keys. The HTML page and the
 markdown form at the same URL carry more, including address history and DNS records,
 because widening a published JSON contract would break every consumer parsing it.

## Content negotiation: one URL, three representations

One URL serves a domain record three ways. A browser gets HTML, a script gets JSON, and
 a `.md` suffix gets markdown.

| Order | Signal | Result |
|---|---|---|
| 1 | path suffix `.json` | JSON, whatever the client sends. |
| 2 | `?format=json` or `?format=html` | The named representation. Overrides everything below. |
| 3 | `Accept` header | JSON when it contains `application/json` and omits `text/html`. |
| 4 | `User-Agent` | JSON for curl, Wget, HTTPie, python-requests, Go-http-client, libwww-perl, axios and okhttp. |
| 5 | default | HTML. |

The `.md` suffix is handled before any of this and always wins. Markdown is
 the cheapest form for a language model to quote accurately, and it carries more than the
 JSON does:

```
$ curl -sS https://issued.live/github.com.md
```

```
# Certificate Transparency record for github.com

Published by issued.live, an independent Certificate Transparency observatory.
issued.live is not github.com, is not affiliated with it, and this record is not
operated on its behalf. Every field is an observation drawn from public logs.

Source: https://issued.live/github.com

## Certificate

| Field | Value |
|---|---|
| Issuer | Amazon |
| Expires | 2026-12-26T23:59:59Z |
| Certificate ID | 9F53ECDDF913AFBA5678C924A95149D2 |
| First seen in CT | not known |
```

Markdown responses answer `Content-Type: text/markdown; charset=utf-8`, carry
 `X-Robots-Tag: noindex, nofollow, noarchive`, and point a
 `Link: rel="canonical"` header at the HTML page. They are a second
 representation of one document, so search engines index the page and agents read the
 markdown.

Each documentation page has the same markdown twin: append `.md` to this
 URL. `/llms.txt` is the machine-readable map of what is worth fetching.

The site root speaks plain text to a script. `curl issued.live` returns a
 short usage summary, so an agent landing on the home page reads what to do next in a few
 hundred bytes.

## Code examples in eleven languages

Every example below fetches the record for `github.com` and prints its
 certificate issuer and expiry. Each one sets an explicit `Accept` header, a
 timeout, and a failure branch.

### curl

```
curl -sS -H "Accept: application/json" https://issued.live/github.com
```

### PHP

```
<?php
$domain = 'github.com';
$ch = curl_init("https://issued.live/{$domain}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Accept: application/json'],
    CURLOPT_TIMEOUT        => 10,
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    fwrite(STDERR, "lookup failed: HTTP {$status}\n");
    exit(1);
}
$d = json_decode($body, true);
printf("%s: %s, expires %s\n", $d['domain'], $d['ssl_issuer'] ?? 'unknown',
       $d['ssl_expires'] ?? 'unknown');
```

### Node.js

```
// Node 18+ has fetch built in; no dependencies needed.
const domain = 'github.com';

const res = await fetch(`https://issued.live/${domain}`, {
  headers: { Accept: 'application/json' },
  signal: AbortSignal.timeout(10_000),
});

if (!res.ok) throw new Error(`lookup failed: HTTP ${res.status}`);

const d = await res.json();
console.log(`${d.domain}: ${d.ssl_issuer ?? 'unknown'}, expires ${d.ssl_expires ?? 'unknown'}`);
```

### Python

```
import requests

domain = "github.com"
r = requests.get(f"https://issued.live/{domain}",
                 headers={"Accept": "application/json"}, timeout=10)
r.raise_for_status()

d = r.json()
print(f"{d['domain']}: {d.get('ssl_issuer') or 'unknown'}, "
      f"expires {d.get('ssl_expires') or 'unknown'}")
```

### Go

```
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type Record struct {
	Domain     string  `json:"domain"`
	SSLIssuer  *string `json:"ssl_issuer"`
	SSLExpires *string `json:"ssl_expires"`
}

func main() {
	client := &http.Client{Timeout: 10 * time.Second}
	req, _ := http.NewRequest("GET", "https://issued.live/github.com", nil)
	req.Header.Set("Accept", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("lookup failed: HTTP %d", resp.StatusCode))
	}

	var r Record
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		panic(err)
	}
	// SSLIssuer is *string: %v on it prints the ADDRESS, not the value.
	issuer := "unknown"
	if r.SSLIssuer != nil {
		issuer = *r.SSLIssuer
	}
	expires := "unknown"
	if r.SSLExpires != nil {
		expires = *r.SSLExpires
	}
	fmt.Printf("%s: %s, expires %s\n", r.Domain, issuer, expires)
}
```

### Java

```
// Java 11+ and java.net.http, so no external dependencies.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

public class Lookup {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10)).build();

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://issued.live/github.com"))
            .header("Accept", "application/json")
            .timeout(Duration.ofSeconds(10))
            .GET().build();

        HttpResponse<String> res =
            client.send(req, HttpResponse.BodyHandlers.ofString());

        if (res.statusCode() != 200) {
            throw new RuntimeException("lookup failed: HTTP " + res.statusCode());
        }
        System.out.println(res.body());
    }
}
```

### Ruby

```
require 'net/http'
require 'json'

uri = URI('https://issued.live/github.com')
req = Net::HTTP::Get.new(uri, 'Accept' => 'application/json')

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 10) do |http|
  http.request(req)
end

abort "lookup failed: HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)

d = JSON.parse(res.body)
puts "#{d['domain']}: #{d['ssl_issuer'] || 'unknown'}, expires #{d['ssl_expires'] || 'unknown'}"
```

### C#

```
// .NET 6+ top-level statements; implicit usings supply System.
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/json"));

var res = await client.GetAsync("https://issued.live/github.com");
res.EnsureSuccessStatusCode();

using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var root = doc.RootElement;

string Text(string key) =>
    root.TryGetProperty(key, out var v) && v.ValueKind == JsonValueKind.String
        ? v.GetString()! : "unknown";

Console.WriteLine($"{Text("domain")}: {Text("ssl_issuer")}, expires {Text("ssl_expires")}");
```

### Rust

```
// Cargo.toml:
//   reqwest     = { version = "0.12", features = ["json", "blocking"] }
//   serde_json  = "1"
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;

    let body: serde_json::Value = client
        .get("https://issued.live/github.com")
        .header("Accept", "application/json")
        .send()?
        .error_for_status()?
        .json()?;

    println!("{}: {}", body["domain"], body["ssl_issuer"]);
    Ok(())
}
```

### PowerShell

```
$d = Invoke-RestMethod -Uri 'https://issued.live/github.com' `
                       -Headers @{ Accept = 'application/json' } `
                       -TimeoutSec 10
"{0}: {1}, expires {2}" -f $d.domain, $d.ssl_issuer, $d.ssl_expires
```

### Bash and jq

```
#!/usr/bin/env bash
# Check a list of domains and print any certificate expiring within 30 days.
set -euo pipefail
cutoff=$(date -u -d '+30 days' +%s)

while read -r domain; do
  json=$(curl -sS --max-time 10 -H 'Accept: application/json' "https://issued.live/${domain}") || continue
  exp=$(jq -r '.ssl_expires // empty' <<<"$json")
  [ -z "$exp" ] && continue
  if [ "$(date -u -d "$exp" +%s)" -lt "$cutoff" ]; then
    echo "$domain expires $exp"
  fi
  sleep 0.2   # be polite
done < domains.txt
```

### Python, with a Basic key

```
import os, requests

key = os.environ["ISSUED_LIVE_KEY"]        # never hard-code a key
s = requests.Session()
s.headers.update({
    "Accept": "application/json",
    "Authorization": f"Bearer {key}",
    "User-Agent": "acme-cert-audit/1.0 (ops@example.com)",
})

for domain in ["github.com", "cloudflare.com", "wikipedia.org"]:
    r = s.get(f"https://issued.live/{domain}", timeout=10)
    if r.status_code == 404:
        print(f"{domain}: no record")
        continue
    r.raise_for_status()
    d = r.json()
    print(f"{d['domain']}: {d['ssl_issuer']}, expires {d['ssl_expires']}")
```

## Errors

Error bodies are JSON and always carry `Cache-Control: no-store`, so an error
 cached at the edge can never outlive the condition that caused it.

Two body shapes exist. The domain lookup returns a single field:

```
{"error":"domain not found"}
```

The endpoints under `/api/v1/`, the feed and the rate limiter return a
 machine-readable code with a human-readable message beside it:

```
{"error":"unauthorized","message":"Supply a key as: Authorization: Bearer <key>"}
```

Branch on `error` and on the status. The `message` is written for
 a person and its wording changes.

| Status | Code | What to do |
|---|---|---|
| 301 | - | The domain path is not canonical: mixed case, a trailing slash, or a trailing `/index.html`. Follow `Location`, and send the canonical form next time to save a round trip. |
| 400 | `bad_request` | Malformed domain, IP or CIDR on a keyed endpoint. Fix the input; retrying will not help. |
| 401 | `unauthorized` | Missing or unrecognized key. **Only the keyed endpoints answer this**, which means the paths under `/api/v1/`. Domain lookups, `/api/feed` and `/api/stats` take no key and never return 401. |
| 404 | - | We hold no record for that domain. Your request was well formed. In most cases we have never seen a certificate for that name. The body reads `domain not found`, or `domain not found or invalid` when the name fails validation. |
| 405 | `method_not_allowed` | A keyed endpoint accepts one method and you sent another. The `Allow` header names the one it takes. |
| 429 | `rate_limited` | Daily free-tier cap reached for your client address. `Retry-After` carries the seconds until the 00:00 UTC reset. Sleep for that long. Retrying on a fixed interval earns another 429. See [rate limits](#limits). |
| 500 | `internal_error` | Our side. Retry once, then report it. |
| 503 | `timeout` | The lookup ran past its deadline. Large addresses and ranges hit this regularly while the service is healthy. On the keyed endpoints and the feeds the body adds `"retryable": true` and a `Retry-After`; a free domain lookup returns the bare `{"error":"timeout"}`. Retry once, then narrow the range or the window. |
| 400 | `window_too_wide` | Only on the Pro feeds at `/api/v1/nrd` and `/api/v1/provisioning`: the `since` to `until` window spans more calendar months than one request may read. The message names the limit; walk the range in slices. |

A 503 arrives instead of a 504 on purpose. The origin sits behind a CDN
 that replaces origin-generated gateway statuses with its own error page, so a 504 would
 reach you stripped of our JSON body.

## Rate limits and the headers that report them

The free, unauthenticated API is capped at **1,000 requests per day per client
 address**, and the counter resets at **00:00 UTC**. An interactive
 visitor will never reach it, and a script doing real work rarely will. The cap stops one
 scraper monopolizing a shared backend.

**Past 1,000 a day, paid plans lift the daily cap entirely and meter
 per minute instead.** They also include daily downloadable files -- domains and DNS
 on Basic, plus DNS by vantage point, certificates and address history on Pro -- which beats
 paging an API for bulk work.
 [Check pricing](/pricing).

| Header | Example | Meaning |
|---|---|---|
| `X-RateLimit-Limit` | `1000` | Your daily allowance. |
| `X-RateLimit-Remaining` | `991` | Requests left today. |
| `X-RateLimit-Reset` | `1789430400` | Unix timestamp of the next reset, which is the next 00:00 UTC. |
| `X-RateLimit-Warning` | a sentence | Sent once fewer than 100 requests remain, so you find out before you are cut off. |
| `Retry-After` | `33131` | Sent with a 429 and with a retryable 503. Seconds to wait. On a 429 it is the time to the UTC day boundary. |

### Where the counters appear

The three `X-RateLimit-*` headers ride on capped responses that a shared
 cache may not store. A successful domain lookup opens with
 `Cache-Control: public, max-age=60, s-maxage=300` and two stale directives, so
 it omits them: a per-client counter inside a shared cache entry would be handed to every
 other visitor for the next five minutes, and a client pacing itself on that number would
 pace itself on someone else's.

Any `no-store` response carries them, which includes every error. A 404 is
 the cheapest way to read your own counter:

```
$ curl -sS -D - -o /dev/null -H 'Accept: application/json' \
    https://issued.live/no-such-domain-here.example | grep -i '^HTTP\|ratelimit'
HTTP/1.1 404 Not Found
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 991
X-RateLimit-Reset: 1789430400
```

Keyed endpoints under `/api/v1/` publish no counters, because there is no
 daily cap on them to report. What bounds them is concurrency -- one advanced query in flight on Plus, two on Pro, at a
 time for your account -- and the
 [Pro reference](/developers/pro#limits) describes it.

### Over the cap

Past the cap you get a `429` with `Retry-After` set to the seconds
 remaining until reset. **Read that header and sleep for the seconds it names.**
 Retrying sooner earns another 429.

```
{"error":"rate_limited","message":"Free API limit of 1000 requests per day reached. It resets at 00:00 UTC. For higher volume, a paid plan lifts it: https://issued.live/pricing"}
```

A browser that trips the cap gets the styled error page instead, with the same
 `Retry-After`. Scripts keep the JSON body above.

### What is never capped

- **Keyed endpoints** under `/api/v1/`, which carry a
 credential and are bounded by concurrency rather than by a daily cap, and the partner
 export under `/export/`, which takes a bearer token.

- `/health`, `/robots.txt`, `/llms.txt`,
 `/sitemap.xml`, `/opensearch.xml`,
 `/site.webmanifest`, the favicons and `/static/`. Capping page
 furniture would spend a visitor's allowance on assets their browser fetched for
 them.

- `/mcp`, which is metered inside its own handler so a refusal arrives as a
 JSON-RPC error object a client can read.

### Etiquette

- **Cache on your side.** Records change over hours. Re-fetching the same
 domain in a loop gets you the same bytes and spends your allowance.

- Serialize bulk work and leave a small delay between requests. A steady trickle is
 always welcome; a burst is what forces limits to exist.

- Send a `User-Agent` that identifies you and gives us a way to make
 contact. If something you run causes a problem, that header is how we ask you about it
 first.

- **Tell us if a limit blocks legitimate work.**
 [Get in touch](/contact) and say what you are building. We would rather
 adjust a limit than lose the use case.

## Frequently asked questions

### Is the API free?

Domain lookups, the statistics endpoint and the certificate feed are free. They need no
 key and no sign-up, up to 1,000 requests a day per client address. A Basic subscription
 lifts that cap and meters per minute instead.

### Does an API key change what a domain lookup returns?

The record is identical. A key raises your allowance and leaves the JSON contract alone.
 Pro adds separate endpoints under `/api/v1/` that return the extended
 record.

### Why are `registered` and `expires` null?

We hold no registration data for that domain. A null means we do not know. Registration
 dates come from registry (WHOIS and RDAP) data, and we never infer them from certificate
 dates or zone files. When a zone file first listed the domain is a separate field,
 `first_zone_seen`.

### Is `first_cert_seen` the registration date?

No, and conflating the two is the most common mistake made with this data. It is the
 earliest time our Certificate Transparency collection recorded a certificate for the
 name. Collection began in August 2026, so a domain registered in 2009 shows an old
 `registered` and a `first_cert_seen` from 2026. For many names our
 records from before 2026-09-05 were lost, so it can be later than our first
 sighting.

### How fresh is the data?

We read Certificate Transparency logs continuously, so certificates appear within
 minutes of being logged. DNS and address data comes from a resolver sweep and can be
 hours old. Every record carries `last_update`; read it to judge freshness.

### Do you return every certificate for a domain, or the current one?

The free lookup returns the most recently observed certificate. The
 [Pro endpoint](/developers/pro) `/api/v1/domain/{domain}` returns
 up to 50 certificates for the same name, with their serial, SANs, key algorithm and JA4X
 fingerprint.

### What counts as a domain?

We resolve every request to its registrable domain using the Public Suffix List, so
 `www.example.co.uk` and `example.co.uk` return the same record.
 Mixed case, a trailing slash and a trailing `/index.html` are normalized with
 a 301 to the canonical path.

### Can I use this commercially?

Yes, within the [terms](/terms). We ask one thing of you: present the data as
 our observation of public logs. It carries no authority from a domain's owner, a
 registry, or a certificate authority.

### Is there an SDK?

There is none, and one would add little. The API is one GET returning flat JSON. Every
 example on this page uses the language's own standard library or its most common HTTP
 client.

## Where to go next

- [Pro API](/developers/pro): reverse IP and CIDR lookups, pattern search,
 batch lookup, certificate and SPKI pivots, JA4X fingerprints and the cursor-paged
 registration feed.

- [Daily file reference](/developers/files): every dataset and column in the
 daily files, and how to apply the deltas.

- [Data feeds and downloads](/developers/feeds): fetching the daily files
 with your API key, and the live newly registered domains feed.

- [Developer hub](/developers): what each API covers and which plan reaches
 it.

Last updated September 17, 2026. Questions the docs do not answer:
 [get in touch](/contact).

↑ Top

---

issued.live, operated by Tuxxin LLC. Source: https://issued.live/developers/api
