Sign in

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"
PlanCredentialAllowance
Freenone1,000 requests a day per client address, resetting at 00:00 UTC.
BasicAuthorization: Bearer <key>60 requests a minute, with no daily cap.
PlusAuthorization: Bearer <key>300 requests a minute, plus reverse IP, the certificate and key pivots, the extended record and the timeline
ProAuthorization: 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

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.

ParameterValuesMeaning
formatjson, htmlQuery parameter. Overrides the Accept header and the User-Agent, so a browser can be handed JSON and a script can be handed the page.
.jsonpath suffixForces JSON. /github.com.json answers JSON to any client, including a browser.
.mdpath suffixReturns 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"
}
StatusBodyWhen
200the recordWe hold a record for the name.
301emptyThe 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"
}
FieldTypeMeaning
as_ofstringWhen the snapshot was built. Anything up to ten minutes old.
totals.domainsnumberRegistrable domains we hold a record for.
totals.certs_24hnumberCertificates observed in the last 24 hours.
totals.names_24hnumberDistinct names carried by those certificates.
totals.entries_24hnumberLog entries read in the last 24 hours, precertificates included.
ingest[]arrayQuarter-hour points of entries and certs, each stamped t.
issuers[]arrayTop issuing CAs as name and count n.
tlds[]arrayTop TLDs by domain count, same name and n shape.
logs.totalnumberCertificate Transparency logs we read.
logs.erroringnumberOf those, how many are failing right now.
logs.behindnumberEntries still to read across every log.
storage.active_bytesnumberBytes the corpus occupies on disk.
storage.free_bytesnumberBytes left on the data volume.
storage.days_to_fullnumberDays of headroom at the current growth rate.
dns.domainsnumberAll registrable domains known. The four fields below describe this population.
dns.ipv4numberOf those, how many have at least one A record.
dns.ipv6numberOf those, how many have at least one AAAA record.
dns.no_dnsnumberOf those, how many have never resolved to any address.
dns.v4_addrsnumberDistinct IPv4 addresses observed. This counts addresses, so it cannot be compared with the domain counts above.
dns.v6_addrsnumberDistinct IPv6 addresses observed.
tld_table[]arrayOne row per TLD, carrying tld, domains, no_dns, ipv4, ipv6, v4_addrs and v6_addrs.
api_exampleobjectA verbatim reply from the domain endpoint, used by the home page. Same shape as a lookup.
api_example_domainstringThe 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.

ParameterDefaultMeaning
limit50Rows 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"
}
FieldTypeMeaning
windowstringThe span the feed covers. Always 30m.
countnumberRows in certificates.
certificates[].seen_atstringWhen we read the log entry carrying this certificate.
certificates[].issuerstringIssuing CA organization.
certificates[].namestringOne name from the certificate.
certificates[].name_countnumberHow 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

FieldTypeMeaning
domainstringThe registrable domain the record describes.
registeredstring | nullThe 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.
expiresstring | nullRegistration expiry, where we hold one. Same null semantics.
ssl_expiresstring | nullNot-after date of the most recently observed certificate.
ssl_certstring | nullCertificate 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_issuerstring | nullIssuing CA organization, as it appears in the certificate.
ipstring | nullAn 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_updatestringThe 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_seenstring | nullThe 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.
trackedbooleanWhether the domain is on our active monitoring list.
first_zone_seenstring | nullWhen 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.

OrderSignalResult
1path suffix .jsonJSON, whatever the client sends.
2?format=json or ?format=htmlThe named representation. Overrides everything below.
3Accept headerJSON when it contains application/json and omits text/html.
4User-AgentJSON for curl, Wget, HTTPie, python-requests, Go-http-client, libwww-perl, axios and okhttp.
5defaultHTML.

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.

StatusCodeWhat 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.
400bad_requestMalformed domain, IP or CIDR on a keyed endpoint. Fix the input; retrying will not help.
401unauthorizedMissing 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.
405method_not_allowedA keyed endpoint accepts one method and you sent another. The Allow header names the one it takes.
429rate_limitedDaily 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.
500internal_errorOur side. Retry once, then report it.
503timeoutThe 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.
400window_too_wideOnly 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.

HeaderExampleMeaning
X-RateLimit-Limit1000Your daily allowance.
X-RateLimit-Remaining991Requests left today.
X-RateLimit-Reset1789430400Unix timestamp of the next reset, which is the next 00:00 UTC.
X-RateLimit-Warninga sentenceSent once fewer than 100 requests remain, so you find out before you are cut off.
Retry-After33131Sent 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

Etiquette

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

Last updated September 17, 2026. Questions the docs do not answer: get in touch.

↑ Top