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 maps out which plan reaches what.
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 to create and rotate a key on the account key page. 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
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. |
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
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
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.
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. |
| 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.
| 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 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-Agentthat 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 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 /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. 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: reverse IP and CIDR lookups, pattern search, batch lookup, certificate and SPKI pivots, JA4X fingerprints and the cursor-paged registration feed.
- Daily file reference: every dataset and column in the daily files, and how to apply the deltas.
- Data feeds and downloads: fetching the daily files with your API key, and the live newly registered domains feed.
- Developer hub: what each API covers and which plan reaches it.
Last updated September 17, 2026. Questions the docs do not answer: get in touch.
↑ Top