<!-- Source: https://issued.live/developers/pro -->
issued.live
 /
 Pro API

# Pro API reference

Every keyed endpoint: reverse IP, pattern search, batch lookup, the full
 domain record, certificate and key pivots, and the provisioning and NRD feeds.

- [What Pro adds](#adds)
- [Authentication](#auth)
- [Rate limits](#limits)
- [Reverse IP](#reverse-ip)
- [Pattern search](#search)
- [Batch lookup](#batch)
- [Domain record](#domain)
- [Timeline](#timeline)
- [Certificate lookup](#cert)
- [Key reuse](#spki)
- [Provisioning feed](#provisioning)
- [Newly registered domains](#nrd)
- [Pivot workflow](#pivot)
- [Errors](#errors)
- [Caps and defaults](#caps)

## What the Pro API adds over the public API

The [public API](/developers/api) answers one question: what do we hold about
 this domain. One name in, one flat record out.

The keyed API answers the reverse questions: which domains resolve to this address, which
 names in the corpus fit this grammar, which certificates carry this public key, and which
 domains were registered, certificated and pointed at a host inside one afternoon.

Every keyed response hands back identifiers that open the next question. A domain record
 carries a `cert_id`; a certificate carries `sans` and an
 `spki_sha256`; a search hit carries `ns` and `cert_id`;
 a reverse-IP row carries a hostname and its registrable domain.

### Who it is for

Threat and fraud investigation, where one observation is only a starting point. You
 have a phishing hostname, a certificate serial from a TLS log, or a nameserver pair from
 a takedown. The work is moving from that to the rest of the infrastructure behind it.

Bulk enrichment of a list you already hold is served better by the
 [daily files](/developers/feeds). Paging an API for the whole corpus is slower
 for you and more expensive for us than one download. The [documentation
 hub](/developers) maps which surface fits which job.

## Authentication: a bearer key in the header on every request

Every endpoint on this page needs a key. Send it as an
 `Authorization` header:

```
curl -sS https://issued.live/api/v1/ip/157.240.17.8 \
  -H "Authorization: Bearer YOUR_KEY"
```

**The key never goes in a URL or a query string.** Query strings land in
 access logs, proxy logs, browser history and `Referer` headers, so there is no
 `?key=` parameter and the header is the only place a key is read. A key in the
 URL therefore leaves the request unauthenticated, and it answers `401`.

A missing or unrecognized key gets `401 unauthorized` with a
 `WWW-Authenticate: Bearer realm="issued.live"` header and this body:

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

Keys come with a plan. See [pricing](/pricing) for what each one includes,
 or [ask us](/contact) if your use case sits outside them.

Keyed replies are served `Cache-Control: no-store, private` with
 `Vary: Authorization`. Cache them on your side if you want them cached;
 nothing between us will do it for you.

## Rate limits and concurrency

Basic reaches none of the endpoints on this page. **There is no daily cap on
 either paid plan.** What bounds this surface is the concurrency limit below, not a
 per-minute ceiling and not a quota you have to ration: the 300 requests a minute Plus
 carries meter the public record API, where a Pro key lifts the free daily cap.

**One advanced query runs at once on Plus, two on Pro, per account.**
 Reverse IP, range, pattern search, batch lookup and the provisioning feed each take a slot;
 the certificate, key, domain, timeline and NRD endpoints are point lookups and take none.
 The limit is yours alone: another customer's traffic does not consume it.

The gate exists because a cold reverse-IP range query can read hundreds of millions of
 rows and hold its database connection for the better part of 18 seconds. Enough of those
 at once would take the whole connection pool and the memory behind it, and every other
 request on the site would then stall. A site-wide ceiling sits above the per-account
 limit as a backstop; you will only meet it if the service as a whole is saturated.

Over the gate a request waits up to five seconds for a slot, then returns
 `503 timeout`. It never queues. An unbounded queue turns one slow query into a
 pile-up where every request holds a connection until they all expire together.

Both doors are metered together. The HTTP API and the MCP tools are two entrances to the
 same query shapes, and they draw on the same per-account slots, so moving to MCP does not
 buy extra concurrency.

### Query cost

Calls are not priced in units, but they are not equal in cost to serve. An unanchored
 pattern, one with no literal prefix, reads the whole key space and measures
 around 350x an anchored search. Anchor your patterns where you can: it is faster for you,
 and it is the difference between a search that returns promptly and one that spends its
 slot.

## Reverse IP: which domains resolve to an address or a small range

GET https://issued.live/api/v1/ip/{ip}
GET https://issued.live/api/v1/range/{cidr}

Answers the reverse of a domain lookup: every hostname our resolvers have seen answer
 with a given address, when it was first seen there, and when it was last seen there.

For a range, **write the prefix slash as a hyphen**. A literal slash is a
 path separator and will not survive routing. A percent-encoded `%2F` works
 too, where your client sends it through intact.

```
curl -sS https://issued.live/api/v1/ip/157.240.17.8 \
  -H "Authorization: Bearer YOUR_KEY"

curl -sS https://issued.live/api/v1/range/157.240.17.0-24 \
  -H "Authorization: Bearer YOUR_KEY"
```

| Parameter | Default | Max | Meaning |
|---|---|---|---|
| `limit` | 500 | 50000 | Hosts returned per request. Out-of-range values are clamped rather than rejected. |
| `after` | - | - | Pagination cursor. Send `?after=` with an empty value to begin a walk, then pass back the `next_cursor` from each response. |
| `format` | - | - | `ndjson` streams one JSON object per line. Also selected by `Accept: application/x-ndjson`, and automatically at `limit` ≥ 5000. |
| `first_seen_from` | - | - | Only hostnames **first observed at this address** at or after this time. RFC3339, `YYYY-MM-DD HH:MM:SS`, `YYYY-MM-DD`, or a unix timestamp. No zone means UTC. |
| `first_seen_to` | - | - | Upper bound on the same field. Either bound may be sent alone. |

`page`, `offset`, `skip` and `cursor` are
 refused with a `400`. All four were once accepted and ignored, which returned
 page one with a 200 however many times you asked for page four.

A malformed or reversed time window is **rejected with a 400** rather than
 adjusted. A limit is a preference, so we clamp it and answer the question you meant. A
 timestamp asserts which rows you want, and substituting a different window is how you end
 up with a gap you cannot see.

### Example response

```
{
  "query":     "157.240.17.8",
  "addresses": 1,
  "count":     2,
  "truncated": false,
  "limit":     500,
  "order":     "recent",
  "hosts": [
    {
      "hostname":   "m.internmc.facebook.com",
      "domain":     "facebook.com",
      "ip":         "157.240.17.8",
      "first_seen": "2026-08-06T21:19:30Z",
      "last_seen":  "2026-08-28T15:44:38Z",
      "observed":   12
    },
    {
      "hostname":   "star.c10r.facebook.com",
      "domain":     "facebook.com",
      "ip":         "157.240.17.8",
      "first_seen": "2026-08-07T02:11:44Z",
      "last_seen":  "2026-08-29T09:03:12Z",
      "observed":   9
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `query` | The address or CIDR as it was parsed. |
| `addresses` | How many addresses the query covered. 1 for `/api/v1/ip/`, up to 256 for a range. |
| `count` | Rows in this response. |
| `truncated` | True when `limit` was reached and more data exists. |
| `order` | Which ordering contract produced this page. See below. |
| `next_cursor` | Present only when another page exists. Its absence ends the walk. |
| `first_seen_from`, `first_seen_to` | Echoed when you sent a window, so you can see it took effect. |
| `hosts[].observed` | How many times we recorded that hostname on that address. |

### Ranges are capped at 256 addresses, and the cap is an engineering limit

A CIDR is expanded into its individual addresses and issued as an `IN` list.
 The address count is therefore the query's cost, and 256 addresses is an IPv4 /24.
 Licensing has nothing to do with it.

The number came out of cost measurements that refuse to form a curve, because cost here is
 dominated by cold granule reads. A /27 of 32 addresses read 383 million rows in
 6.8 s, a /26 of 64 addresses read 5.1 million in 188 ms, and a /24 of 256
 addresses read 15.7 million in 365 ms. The same /24 had read 669 million rows
 cold minutes earlier, so page cache decides and prefix length follows it.

Anything wider answers `400 bad_request` naming the widest prefix allowed.
 To sweep something larger, iterate a /24 at a time and **serialize the
 requests**. Running them concurrently makes each one slower rather than finishing
 sooner, and the gate admits two at a time for your account anyway.

### How fast a lookup is, and what makes it slow

Lookups read an address-ordered copy of the corpus, so a single address is a direct seek
 rather than a scan. Typical single-address latency is tens of milliseconds. A busy hosting
 address carrying millions of names is still fast.

Cache results on your side. This data changes over hours.

### Time-bounding reduces the number of pages, and a single page costs the same

`first_seen` sits outside the table's sort key, so the filter skips none of
 the reading. Measured on an address holding 18.9 million observations, one page of
 500 costs 4.24 s unfiltered and 4.38 s with a six-hour window.

What a window changes is how many requests you need.
 `3.33.130.190` carries 28,705,254 hostnames, roughly 575 pages at the maximum
 limit, and the same address restricted to a six-hour provisioning window is one or two
 pages. For "what appeared here on Tuesday afternoon", that is the difference between a feasible
 query and an infeasible one.

### What first_seen means before you build a window around it

`first_seen` is the earliest time our resolvers saw that hostname
 answer with that address. Two consequences matter:

- These observations begin in **August 2026**. A window earlier than that
 returns nothing, because we were not looking yet. An empty page is an answer about the
 window you asked for. Check the window before concluding the address was unused.

- It is a minimum over everything we have seen, so it can move
 **earlier** when a late-arriving observation carries an older timestamp. A
 hostname can therefore leave a window it previously satisfied. Treat a windowed result
 as a snapshot of what we knew when you asked. Paging stays safe: the cursor walks an
 immutable key, so nothing slips behind it mid-walk.

### Pagination: two orderings, and only one of them resumes

The cap is per request; the cursor is how you get past it. Some addresses carry tens of
 millions of names, far more than any single response should hold.

```
curl -sS "https://issued.live/api/v1/ip/104.18.22.186?after=&limit=50000" \
  -H "Authorization: Bearer YOUR_KEY"
```

| `order` | When | Sorted by | Resumable |
|---|---|---|---|
| `recent` | no `after` parameter | `last_seen` descending | No |
| `key` | `after` present, even empty | (ip, rd_rev, fqdn_rev) ascending | Yes |

Paging is an explicit mode rather than something that switches on result size, because
 the two orderings cannot be mixed inside one walk without dropping rows. Recency ordering sorts by `last_seen`, which we rewrite whenever we
 re-observe a name. A row can therefore move between pages and be served twice or
 skipped.

Key order is the table's primary key, and none of its three columns ever changes for a
 given observation. Resuming is a seek that prunes whole granules ahead of the read.

A cursor is opaque. Treat it as a token to hand back, rather than something to parse or
 construct.

**`next_cursor` is present only when another page exists, and its
 absence is the only end-of-data signal.** We read one row past your limit to decide. A short page can therefore carry a cursor,
 and a full page with nothing behind it carries none.

### Streaming large answers as NDJSON

At `limit` ≥ 5000, or on `?format=ndjson`, the answer
 streams as newline-delimited JSON. Neither side holds it in memory, and you can start work
 on the first row instead of waiting for the last.

```
{"query":"104.18.22.186","addresses":1,"limit":50000,"format":"ndjson","order":"key"}
{"hostname":"app.airquote.ac","domain":"airquote.ac","ip":"104.18.22.186","first_seen":"2026-08-11T04:02:51Z","last_seen":"2026-09-01T22:14:07Z","observed":6}
{"end":true,"count":50000,"next_cursor":"OjpmZmZmOjEwNC4xOC4yMi4xODYf..."}
```

Records always carry `hostname` and never carry `end`, so the
 trailer is unambiguous. A clean finish with zero rows still gets a trailer.

**The trailer is written only on a clean finish.** If the stream dies
 mid-flight there is no trailer and no cursor, and that absence is the error signal; the
 headers are already sent by then, so no status code is available to carry it. Resuming
 from a cursor you were never given would skip everything between the failure and the last
 row you received.

### Iterating a larger range

```
const key = process.env.ISSUED_LIVE_KEY;

// Serialized on purpose. Concurrency does not help here, and the gate admits two at a time.
for (let third = 0; third < 4; third++) {
  const cidr = `157.240.${16 + third}.0-24`;
  const res = await fetch(`https://issued.live/api/v1/range/${cidr}`, {
    headers: { Authorization: `Bearer ${key}` },
    signal: AbortSignal.timeout(40_000),
  });
  // 503, never 504: this 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.
  if (res.status === 503) { console.warn(`${cidr}: timed out, retry later`); continue; }
  if (!res.ok) throw new Error(`${cidr}: HTTP ${res.status}`);
  const { hosts } = await res.json();
  console.log(cidr, hosts.length);
}
```

## Pattern search: find registrable domains whose name fits a grammar

GET https://issued.live/api/v1/search?name={pattern}

Turns a naming hypothesis into a query. If you are testing "a short word followed by
 `agent`, in `.com`", you cannot enumerate the candidates yourself:
 there is no wordlist, and the space is combinatorial. The match has to happen next to the
 data.

```
curl -sS "https://issued.live/api/v1/search?name=[a-z]{4,10}agent.com&limit=100" \
  -H "Authorization: Bearer YOUR_KEY"
```

| Parameter | Default | Max | Meaning |
|---|---|---|---|
| `name` | required | 128 chars | The pattern. At most 8 labels. A missing or empty value is a 400. |
| `limit` | 500 | 1000 | Candidates examined per page. Omitting it gives 500. |
| `after` | - | - | Cursor. Omit it to start, then pass back `next_cursor`. |
| `ns` | - | - | Keep only rows whose nameserver set matches, by exact name or by parent domain. |
| `first_seen_from` | 30 days back | - | Switches to window mode. Same time formats as the reverse-IP window. |
| `first_seen_to` | now | - | Upper bound in window mode, always clamped to now. |

### The pattern language

Write the pattern **forwards**, the way you say the name. Per label:

| Syntax | Matches | Example |
|---|---|---|
| `*` | Zero or more of `a-z`, `0-9`, `-` | `*agent.com` |
| `?` | Exactly one of those characters | `loop?.com` |
| `[a-z]{4,10}` | A counted run from a character class | `[a-z]{4,10}agent.com` |
| literal | Itself | `loop-lumen.com` |

A pattern must include a TLD. `*agent` alone could mean a name or a suffix,
 and guessing would search the wrong thing. Write `*agent.*` to mean every
 TLD.

The count on a character class is **required and must be bounded**, with an
 upper bound of 64. `[a-z]+` and `[a-z]{4,}` are refused, because an
 open bound is how a pattern silently becomes a full scan. Grouping, alternation and
 negation are unsupported, and the compiled expression comes back as `regex` so
 you can see exactly what ran.

### Naming a TLD is worth about 60x

Names are stored suffix-first, so a pattern that pins the TLD becomes a key range, and
 one that does not has to read the whole key space. Measured:

| Pattern | Anchored | Scan | Cost |
|---|---|---|---|
| `loop-*.com` | yes, `com.loop-` | 0.03 s | cheap |
| `[a-z]{4,10}agent.com` | yes, `com.` | 0.7 s | cheap |
| `*agent.*` | no | 1-3.5 s | ~350x an anchored scan |

The response reports `anchored` and `key_prefix`, so you can see
 which case you are in. An unanchored search costs more against the meter because it costs
 more to answer.

### Window mode and corpus mode

Sending `first_seen_from` or `first_seen_to` switches to
 **window mode**, which searches the registration feed by date. The date is
 the leading sort key there, so the same question is an index range measured at
 0.094 s. It is the right mode for "which names matching this grammar appeared last
 week".

Without a window you get **corpus mode**: every name we hold, paged by key.
 The response says which you got in `mode`.

Sending only `first_seen_to` defaults the lower bound to 30 days back, and the
 reply echoes the window that was applied.

Cursors are not interchangeable between the modes. One carries a position in time and
 one does not. A cursor from the wrong mode gets a 400, rather than a silent restart of
 the walk.

### count, scanned, and how to tell when a walk is finished

`scanned` is how many candidate names the page examined.
 `count` is how many it returned. They differ when `ns=` filtered rows
 out after the page was drawn.

Keep following `next_cursor` until it is absent. The cursor advances over
 candidates, so a page that returns nothing still moves you forward.

```
{
  "pattern":    "[a-z]{4,10}agent.com",
  "regex":      "^com\\.[a-z]{4,10}agent$",
  "mode":       "corpus",
  "anchored":   true,
  "key_prefix": "com.",
  "count":      100,
  "scanned":    100,
  "limit":      100,
  "truncated":  true,
  "next_cursor": "H2NvbS5hYmJpbnNhZ2VudA",
  "domains": [
    {
      "domain":          "abbinsagent.com",
      "tld":             "com",
      "first_zone_seen": "2026-09-05T22:00:00Z",
      "first_cert_seen": "2026-09-06T04:21:09Z",
      "cert_id":         "b800bf9f6375b6825ca2b92d97bbb06f",
      "ns": ["ns1.zoom.ph", "ns2.zoom.ph"]
    }
  ]
}
```

Every row carries `ns` and `cert_id`, which makes a name match
 actionable. A shared nameserver pair, a same-day registration and a name that fits the grammar are
 together a far stronger signal than any one of them alone.
 `cert_id` chains straight into [the certificate endpoint](#cert).

## Batch lookup: up to 500 domains in one POST

POST https://issued.live/api/v1/domains

The same core record as a single lookup, for a whole candidate list in one request. A
 432-name list becomes one call and a fraction of a second.

```
curl -sS https://issued.live/api/v1/domains \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains":["example.com","loop-lumen.com","github.com"]}'
```

| Parameter | Where | Meaning |
|---|---|---|
| `domains` | body | Array of names. At least one, or the reply is a 400. |
| `include` | query | Comma-separated: `addresses`, `certificates`, `timing`. Each adds a sub-list of up to 10 entries per domain. An unrecognized value returns a 400 naming the three that exist. |

| Cap | Value | Why |
|---|---|---|
| Domains per request | 500 | The reply is assembled in memory. |
| Domains with any `include` | 100 | Each domain then carries up to three sub-lists, and 500 of those is the shape that will not buffer. |
| Sub-list entries per domain | 10 | Same reason. |
| Request body | 512 KiB | 500 names at the maximum legal hostname length is about 127 KiB, so this is generous. Over it is a `413 too_large`. |

Sending more names than the cap **truncates to the cap and sets
 `truncated`**. A caller with 900 names gets the first 500 and a clear
 signal to send the rest. A `Content-Type` other than
 `application/json` is a `415`.

### Every input gets a row back, in the order you sent it

Duplicates included, so you can zip the reply against your list without normalizing
 anything. A duplicate is answered twice and looked up once.

| Status | Meaning |
|---|---|
| `found` | We hold a record. |
| `not_found` | We looked and hold nothing. |
| `rejected` | The input does not parse as a registrable domain, and `reason` says so. **We did not look it up.** |

Those last two are deliberately different. A batch that dropped unparseable input would
 let you conclude a domain is absent from the corpus when it was never queried.

```
{
  "count":     3,
  "found":     2,
  "not_found": 0,
  "rejected":  1,
  "limit":     500,
  "truncated": false,
  "domains": [
    {
      "query":  "example.com",
      "status": "found",
      "domain": "example.com",
      "record": {
        "domain":          "example.com",
        "tld":             "com",
        "first_zone_seen": "2026-08-05T19:16:00Z",
        "first_cert_seen": "2026-08-06T21:19:30Z",
        "cert_id":         "9f53ecddf913afba5678c924a95149d2",
        "ns": ["a.iana-servers.net", "b.iana-servers.net"]
      }
    },
    {
      "query":  "not a domain",
      "status": "rejected",
      "reason": "not a registrable domain"
    }
  ]
}
```

With `?include=addresses,certificates,timing` each found row also carries
 `addresses`, `certificates` and `registration_timing`,
 and the response echoes `include` and `sub_limit`. A sub-query that fails drops its decoration rather than failing the whole call. An empty
 sub-list against an echoed `include` therefore reads as "none held".

## Timeline: every dated event for one domain, with the gap between them

GET https://issued.live/api/v1/timeline/{domain}

Registration, first appearance in a zone file, certificate issuances, address changes and
 DNS changes, merged into one list in time order. Each event after the first carries
 `gap_seconds`, the interval since the previous event.

Reconstructing this by hand means four separate calls and a merge. The interval is the part
 that is hard to get by hand and easy to read once it is there: bulk-provisioned
 infrastructure produces tight, regular gaps that a person does not.

### Read gap_seconds together with precision

Our sources record time at different resolutions, and a gap is only as good as the two
 timestamps it sits between. Every event states which it came from.

| `precision` | Source | What the gap means |
|---|---|---|
| `exact` | Certificate Transparency | `not_before` is the issuing CA's own clock, accurate to the second. A gap between two certificate events is the subject's behavior. |
| `day` | Registry and zone files | Dated to the day. Gaps under 24 hours carry no information. |
| `observed` | Our resolvers | The time we saw the change, not when it happened. The gap reflects our re-resolve interval. A domain may have changed minutes after we looked and we would record it at the next sweep. |

A gap between two `exact` events is evidence. A gap between two
 `observed` events is a measurement of our scan schedule. Filter on
 `precision` before fingerprinting.

### Response

```
{
  "domain": "example.com",
  "count": 5,
  "cert_window_days": 14,
  "events": [
    { "at": "2026-09-01T00:00:00Z", "event": "domain_registered",
      "detail": "registrar: Example Registrar, Inc.",
      "source": "registry", "precision": "day" },
    { "at": "2026-09-06T02:28:42Z", "event": "certificate_issued",
      "detail": "Let's Encrypt, 2 names, valid 90d",
      "source": "ct", "precision": "exact", "gap_seconds": 440922 },
    { "at": "2026-09-06T03:28:41Z", "event": "certificate_issued",
      "detail": "Let's Encrypt, 2 names, valid 90d",
      "source": "ct", "precision": "exact", "gap_seconds": 3599 },
    { "at": "2026-09-06T04:28:48Z", "event": "certificate_issued",
      "detail": "Let's Encrypt, 2 names, valid 90d",
      "source": "ct", "precision": "exact", "gap_seconds": 3607 },
    { "at": "2026-09-08T05:05:21Z", "event": "addresses_observed",
      "detail": "8 addresses: 192.0.2.10, 192.0.2.11, 198.51.100.7 ...",
      "source": "resolver", "precision": "observed", "gap_seconds": 174993 }
  ]
}
```

The three certificate events above are 3599 and 3607 seconds apart. That is an hourly job
 reissuing a certificate, and it is the pattern this endpoint exists to surface.

### Events

| `event` | Meaning |
|---|---|
| `domain_registered` | Registry creation date. Absent where we hold none. |
| `first_seen_in_zone` | First appearance in a zone file we load. |
| `certificate_issued` | One issuance, with issuer, how many names on this domain it covered, and its validity in days. Precertificate and final certificate are one event. |
| `addresses_observed` | One or more addresses first seen at that moment. A rotation that swaps eight addresses at once is one event, not eight. |
| `dns_<type>_changed` | A DNS answer changed, with the old and new values. `dns_ns_changed`, `dns_a_changed` and so on. |

### Limits

Certificate events cover the last **14 days**, matching the window on
 [certificate lookup](#cert); `cert_window_days` repeats that figure in
 every response so a short timeline is not mistaken for a quiet domain. Older certificates
 are in the archived [daily files](/developers/files#certificates). Address and DNS
 history reach back further, and registration has no expiry.

A response holds at most 2,000 events. A domain behind a large shared certificate can
 exceed that; the daily files are the better tool for those.

A `404` means we hold no dated events for the name. It does not mean the domain
 is inactive.

## Full domain record: everything we hold for one name, in one request

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

The combined record instead of four round trips: the public record, the registration
 timing, every address the name has been seen on, and its certificates.

The name is folded to lower case and resolved to its registrable domain through the
 Public Suffix List. `WWW.Example.COM` and `example.com` reach the
 same record.

```
curl -sS https://issued.live/api/v1/domain/hoteles-andalucia.com \
  -H "Authorization: Bearer YOUR_KEY"
```

| Part | Cap | Meaning |
|---|---|---|
| `record` | one | The same fields the public API returns for the name. |
| `registration_timing` | one | Present when the name was observed as newly registered. Omitted otherwise. |
| `addresses` | 50 | Address history, newest first. |
| `certificates` | 50 | Certificates covering the name, newest first. |
| `counts` | - | How many of each this response carried. |

```
{
  "domain": "hoteles-andalucia.com",
  "record": {
    "domain":          "hoteles-andalucia.com",
    "registered":      null,
    "expires":         null,
    "ssl_expires":     "2026-12-06T13:51:23Z",
    "ssl_cert":        "3B1A7C55D0E9F2184A6C90BB77E1D4C2",
    "ssl_issuer":      "Let's Encrypt",
    "ip":              "178.32.125.215",
    "last_update":     "2026-09-07T15:02:11Z",
    "first_cert_seen": "2026-09-07T14:49:59Z",
    "tracked":         false,
    "first_zone_seen": null
  },
  "registration_timing": {
    "first_zone_seen": "2026-09-07T06:03:27Z",
    "first_cert_seen": "2026-09-07T13:55:02Z",
    "hours_to_cert":   7.86,
    "rdap_created":    "2026-09-07T05:58:14Z"
  },
  "addresses": [
    { "ip": "178.32.125.215", "first_seen": "2026-08-24T14:33:59Z",
      "last_seen": "2026-08-31T06:19:45Z", "observed": 3, "current": true }
  ],
  "certificates": [ { "cert_id": "3b1a7c55d0e9f2184a6c90bb77e1d4c2", "...": "see below" } ],
  "counts": { "addresses": 1, "certificates": 1 }
}
```

`null` in `registered` or `expires` means we hold no
 registration record for that name. A registered domain can still show null there. We never
 infer either field from certificate dates.

`current` on an address marks the one we believe is live now. The others are
 history, which is the reason to keep them.

A name with no record, no addresses and no certificates answers
 `404 not_found`. That is an answer about the corpus rather than a fault in the
 request.

## Certificate lookup: one certificate, by either identifier

GET https://issued.live/api/v1/cert/{cert_id or sha256}

Returns what was issued: the subject, the SANs, the key, the validity window, the JA4X
 fingerprint and the logs it appeared in.

The path accepts **two widths, and which one you hold depends on where you got
 it**. Case is ignored for both.

| Length | Field | What it is |
|---|---|---|
| 32 hex | `cert_id` | Our certificate key: the first half of a SHA-256 over the normalized TBSCertificate, and the value a domain record's `ssl_cert` publishes. Present on **every** certificate we hold. Use this one. |
| 64 hex | `cert_sha256` | SHA-256 of the DER-encoded certificate: what `openssl x509 -fingerprint -sha256` prints, and what other CT tools call the certificate hash. Recorded only where we observed the final certificate, so it is **absent for precert-only entries** and omitted from the response when it is. |

Any other length is a `400` that names both accepted widths. If you are
 chaining from a domain lookup you already hold the 32-hex form: take
 `record.ssl_cert` and send it straight here.

```
curl -sS https://issued.live/api/v1/cert/0cac1fd947ba8a3f2798f46772ac981db6fda26784c76d40e245cb63d8a975d4 \
  -H "Authorization: Bearer YOUR_KEY"
```

```
{
  "cert_id":       "3b1a7c55d0e9f2184a6c90bb77e1d4c2",
  "cert_sha256":   "0cac1fd947ba8a3f2798f46772ac981db6fda26784c76d40e245cb63d8a975d4",
  "serial":        "052d2574f33bd28591832dcf7c10f990ddfb",
  "spki_sha256":   "746b5f9f4ac791f00e526234653f6f369b2b290b444a7c1dca810c9d7886a76e",
  "issuer":        "CN=YE2,O=Let's Encrypt,C=US",
  "issuer_org":    "Let's Encrypt",
  "subject_cn":    "hoteles-andalucia.com",
  "key_alg":       "ECDSA",
  "key_bits":      256,
  "signature_alg": "ECDSA-SHA384",
  "valid_from":    "2026-09-07T13:51:24Z",
  "valid_until":   "2026-12-06T13:51:23Z",
  "sans":          ["*.hoteles-andalucia.com", "hoteles-andalucia.com"],
  "san_count":     2,
  "wildcard":      true,
  "ja4x":          "2e16925cec5b_3525cf66b29e_73cc281",
  "first_observed": "2026-09-07T13:55:02Z",
  "last_observed":  "2026-09-07T13:55:02Z",
  "ct_logs_seen":   2
}
```

### Retention: 14 days here, forever in the files

Full certificate rows and the name-to-certificate index are queryable here for
 **14 days**. Registration data in the domain record has no expiry; it is only
 the certificate detail that ages out of the database.

Nothing is lost when it does. Every daily certificates file is kept in cold storage
 indefinitely, so a date beyond the 14-day window is a file request rather than a gap.

Those entries come back through `/api/v1/domain/` with a `detail`
 field saying the full row has aged out. "We no longer hold the certificate" and "this
 domain had no certificate" are different answers, and only one of them is evidence.

A `404` from `/api/v1/cert/` for an older id reports the retention
 limit and says nothing about whether the certificate existed. The 404 message names the width you sent. A pasted 64-hex digest that we hold only as a
 precert is therefore distinguishable from a genuine miss.

## Key reuse: every certificate sharing one public key

GET https://issued.live/api/v1/spki/{sha256}

`spki_sha256` is the SHA-256 of the certificate's SubjectPublicKeyInfo. Two
 certificates carrying the same value were issued for the **same key pair**.

Usually that is a renewal. Sometimes it is one operator across unrelated names. Either
 way it is a stronger link than a shared name or address, because whoever holds one private
 key holds them all.

**Coverage begins 7 September 2026.** Certificates first observed before that
 date carry no key hash and cannot be pivoted on: they were ingested before the field
 existed, and it cannot be backfilled because deriving it needs the certificate bytes, which
 are not what we retain. Everything observed from 8 September onward has it, so the gap is
 the oldest end of the window and it shrinks as those entries age out of the 14-day
 retention above. A key that appears only in the uncovered period looks the same as a key we
 never saw; treat a thin result from an old pivot as unknown rather than as absence.

| Parameter | Default | Max | Meaning |
|---|---|---|---|
| path segment | required | 64 hex | The key hash. Any other length is a 400. |
| `limit` | 500 | 500 | Certificates returned. Ask for fewer to get fewer. |

```
curl -sS "https://issued.live/api/v1/spki/746b5f9f4ac791f00e526234653f6f369b2b290b444a7c1dca810c9d7886a76e?limit=100" \
  -H "Authorization: Bearer YOUR_KEY"
```

```
{
  "spki_sha256":  "746b5f9f4ac791f00e526234653f6f369b2b290b444a7c1dca810c9d7886a76e",
  "count":        2,
  "truncated":    false,
  "certificates": [
    {
      "cert_id":    "3b1a7c55d0e9f2184a6c90bb77e1d4c2",
      "subject_cn": "hoteles-andalucia.com",
      "issuer_org": "Let's Encrypt",
      "valid_from": "2026-09-07T13:51:24Z",
      "sans":       ["*.hoteles-andalucia.com", "hoteles-andalucia.com"]
    }
  ],
  "coverage": "Only certificates ingested after 2026-09-07 carry a recorded SubjectPublicKeyInfo hash; earlier ones cannot be matched on it."
}
```

**Read the `coverage` field before drawing a conclusion.** We
 began recording the key hash on 2026-09-07. Certificates ingested before that date sit in
 the corpus and cannot be matched on it.

A small result therefore means "few matches among certificates we have hashed". Treating
 it as evidence that the key is unused elsewhere would overstate it. The field ships on
 every response, so that distinction is never left to inference, and coverage grows daily
 as certificates are re-observed.

## Provisioning feed: domains registered, certificated and resolved inside one window

GET https://issued.live/api/v1/provisioning

Any one of those three facts is ordinary. All three within an afternoon is a domain being
 stood up to be used.

Nothing new is collected for this. The registration and certificate legs are one row
 already, and the DNS leg is the first answer our resolvers recorded.

```
curl -sS "https://issued.live/api/v1/provisioning?max_hours_to_cert=6&cert_to_dns_max=6" \
  -H "Authorization: Bearer YOUR_KEY"
```

| Parameter | Default | Max | Meaning |
|---|---|---|---|
| `max_hours_to_cert` | 6 | 168 | Hours from zone-file appearance to certificate. **This bound is what keeps the query affordable**: over a 72-hour window there are about 592 candidates at 1 hour, 11,206 at 6 and 114,531 at 24. Over the ceiling it clamps and the reply echoes what was applied. Zero or negative is a 400. |
| `cert_to_dns_max` | - | - | Hours from certificate to first DNS answer. Applied after the page is drawn. See below. |
| `since` | 72 hours back | - | Start of the candidate window. RFC3339, `YYYY-MM-DD HH:MM:SS`, `YYYY-MM-DD`, or unix. |
| `until` | now | - | End of it, always clamped to now. At most 13 calendar months per request. |
| `include_cert_before_zone` | off | - | `1` or `true` includes domains certificated before they appeared in a zone file. |
| `tld` | - | - | One TLD. A leading dot and any case are accepted. |
| `ns` | - | - | Nameserver filter, by exact name or parent domain. A post-filter, like `cert_to_dns_max`. |
| `limit` | 500 | 500 | Candidates per page. |
| `after` | - | - | Cursor. Omit it to start. A cursor from another endpoint is a 400. |

One page, from a call that also sent `include_cert_before_zone=1`:

```
{
  "count":       1,
  "scanned":     100,
  "limit":       500,
  "caught_up":   true,
  "next_cursor": "MTc4ODM3MDIwNx9jb20ubG9vcC1sdW1lbg",
  "window":  { "from": "2026-09-11T06:00:00Z", "to": "2026-09-14T06:00:00Z" },
  "filters": {
    "max_hours_to_cert": 6,
    "cert_to_dns_max":   6,
    "include_cert_before_zone": true
  },
  "coverage": {
    "source_lag_minutes":    15,
    "dns_observed_pct_24h":  36.9,
    "dns_observed_pct_72h":  83.9,
    "dns_observed_pct_168h": 96.1,
    "dns_note":    "... see below ...",
    "signal_note": "... see below ...",
    "source_note": "... see below ..."
  },
  "domains": [
    {
      "domain":              "loop-lumen.com",
      "tld":                 "com",
      "first_zone_seen":     "2026-09-02T06:03:27Z",
      "rdap_created":        "2026-09-01T10:44:00Z",
      "first_cert_seen":     "2026-09-01T11:04:26Z",
      "first_dns_answer":    "2026-09-01T17:40:22Z",
      "dns_status":          "observed",
      "hours_zone_to_cert":  -18.98,
      "hours_cert_to_dns":   6.6,
      "sequence":            "cert_before_zone",
      "cert_id":             "c41f8b02ad6e4470b95cfa2118d7e330",
      "ns": ["teresa.ns.cloudflare.com", "theo.ns.cloudflare.com"]
    }
  ]
}
```

### Why the DNS gap filter drops recent rows

`first_dns_answer` is when **our** resolvers first saw the name
 answer, so our scan latency sets its lower bound rather than the operator's provisioning.
 Measured over candidates certificated within 6 hours of registration:

| Window | Candidates with a DNS observation |
|---|---|
| last 24 hours | **36.9%** |
| last 72 hours | 83.9% |
| last week | 96.1% |

In a recent window **most rows have no DNS leg yet**, and
 `cert_to_dns_max` excludes them. Those rows carry
 `dns_status: "not_yet_observed"`, which means we have not recorded an answer.
 It says nothing about whether the domain resolves.

Widen the window before concluding a tight provisioning gap is absent. The same figures
 ship in every response under `coverage`, alongside a 15-minute source lag: the
 candidate view refreshes on that interval.

The signal is episodic. Sub-hour certification is about 1.1% of certified registrations,
 against 8.4% taking over a week. An empty page is a real answer about the window you asked
 for.

### Two timestamps, kept separate on purpose

`first_zone_seen` is our own sighting: when the name first appeared in a zone
 file. `rdap_created` is the registry's own creation date, where we hold one.

They are reported separately, and there is deliberately no field called "registered".
 For a domain with no registry record, such a field would be the zone sighting wearing a
 registry's name. You would have no way to tell.

`hours_zone_to_cert` is signed, and `sequence` names the ordering.
 `cert_before_zone` is a certificate issued for a name not yet in any zone file,
 which is a stronger signal than the ordinary case. It stays off by default so the
 existing feed contract is unchanged.

`scanned` counts candidates examined and `count` counts rows
 returned. Both post-filters remove rows after the page is drawn, so follow
 `next_cursor` until `caught_up` is true.

## Newly registered domains: a gap-free cursor feed

GET https://issued.live/api/v1/nrd

Returns newly registered domains in **ascending time order from a cursor**.
 A consumer resumes exactly where it stopped and can never skip an event, however long it
 was away.

The ordering columns are immutable: a registration event does not get re-registered. That
 is what lets a bounded window and a cursor coexist here safely.

```
curl -sS "https://issued.live/api/v1/nrd?certified=1&max_hours_to_cert=1&limit=100" \
  -H "Authorization: Bearer YOUR_KEY"
```

| Parameter | Default | Max | Meaning |
|---|---|---|---|
| `after` | 48 hours back | - | Cursor, and your position within the window. It is what guarantees you never skip or repeat an event. A cursor dated in the future is refused with `400 bad_cursor`. |
| `since` | the cursor | - | Lower bound. RFC3339, `YYYY-MM-DD HH:MM:SS`, `YYYY-MM-DD`, or unix; UTC when no zone is given. Whichever of `since` and your cursor is further along wins, so resuming never re-serves rows. |
| `until` | now | - | Upper bound, always clamped to now. |
| `tld` | - | - | Restrict to one TLD. A leading dot and any case are accepted. |
| `limit` | 50 | 500 | Rows per page. An explicit `limit=500` returns 500. |
| `certified` | - | - | `1` or `true` restricts to NRDs that have already had a certificate issued, a much smaller set. |
| `max_hours_to_cert` | - | - | Hours from first zone-file appearance to certificate. **Requires `certified=1`**; sending it alone is a 400 rather than a filter that silently does nothing. |

A domain that appears in a certificate minutes after it is registered was almost
 certainly registered in order to be certificated. Sub-hour certification runs at about 1.1%
 of certified registrations, against 8.4% taking over a week.

```
{
  "certified":   true,
  "count":       100,
  "next_cursor": "1787957711.click.joffulm",
  "caught_up":   false,
  "window":   { "since": "2026-08-28T22:00:00Z", "until": "2026-08-30T22:00:00Z" },
  "coverage": { "from": "2026-07-02T00:00:00Z", "to": "2026-08-30T22:04:00Z",
                "max_span_months": 13 },
  "domains": [
    { "seen_at": "2026-08-28T22:55:11Z", "domain": "joffulm.click", "tld": "click" }
  ]
}
```

| Field | Meaning |
|---|---|
| `next_cursor` | Pass as `?after=` on the next call. **Always store it**, including when `caught_up` is true. |
| `caught_up` | True when there is nothing further in the window right now. Determined by reading one row past your `limit`, so it is an observation rather than a guess. It is a pause, so poll again later with the same cursor. |
| `window` | The `since` and `until` bounds in force, after defaults and clamping. |
| `coverage` | The span this feed can answer for, read from the data on every request. Use it to tell "no domains matched" from "we hold no history that far back". |
| `tld` | Echoed when you filtered on one. |

### A window may span 13 calendar months per request

A wider one is refused with `400 window_too_wide` naming the limit. **The
 ceiling applies per request and caps nothing about how much history you may read.**
 Set `until` at most 13 months after `since`, drain that slice with
 `after`, then move both bounds forward.

We refuse rather than silently narrowing your window, because a window you did not ask
 for is a gap you cannot see. The same ceiling applies to
 [the provisioning feed](#provisioning), which reads a table partitioned the same
 way.

`page`, `offset`, `skip`, `cursor` and
 `date` are **refused with a 400**, and all five were once accepted
 and ignored. `cursor` is the name most people try first, and it returned page one
 every time, with a 200. If you have code paging this feed with `?cursor=`, it
 has been reading the same page in a loop; rename it to `after`.

### Draining the feed correctly

```
import os, time, requests

key    = os.environ["ISSUED_LIVE_KEY"]
cursor = load_cursor()          # persist this; it is the whole point

while True:
    r = requests.get("https://issued.live/api/v1/nrd",
                     params={"certified": 1, "limit": 500, "after": cursor},
                     headers={"Authorization": f"Bearer {key}"}, timeout=30)
    r.raise_for_status()
    page = r.json()

    for d in page["domains"]:
        handle(d["domain"], d["seen_at"])

    # Save the cursor even on a short page. That is how you resume without gaps.
    cursor = page["next_cursor"]
    save_cursor(cursor)

    # caught_up means "nothing more right now". Back off and poll.
    time.sleep(60 if page["caught_up"] else 1)
```

For history in bulk, the [daily files](/developers/feeds) beat draining months
 through this feed. They are cheaper for both of us.

## The pivot workflow: from one certificate to the infrastructure around it

This is the reason to hold a key. One observation is a starting point, and the endpoints
 above exist to turn it into a set.

### What each link is worth

| Link | Strength | Why |
|---|---|---|
| Shared address | Weak alone | A CDN address carries millions of unrelated names. Co-location proves shared hosting. |
| Shared certificate (`sans`) | Strong | Names packed onto one certificate were issued together, by one party, for one deployment. |
| Shared key (`spki_sha256`) | Strongest | One key pair across certificates means one holder of one private key. |
| Shared nameservers (`ns`) | Moderate | Strong when the pair is account-specific, weak when it is a large provider's default. |
| `ja4x` | Corroboration | It fingerprints the CA's issuance software. 235 distinct values across 20.3 million certificates, so a pivot on it would return a third of Let's Encrypt. We report it and offer no pivot on it. |

### Why SANs survive a reverse proxy

A certificate's `sans` array is the list of names it covers, which makes it a
 linkage that outlives a CDN. A universal-SSL certificate packs several customer zones onto one certificate. Those
 zones share a certificate we can see, even while they share an address with a million
 unrelated sites.

Co-tenancy on a certificate usually indicates a relationship. Co-location on an address
 rarely does.

### A worked pivot, from one hostname to the set around it

Start with a single phishing hostname and end with a candidate list, its nameserver
 pattern and the addresses behind it.

```
import os, requests

H   = {"Authorization": f"Bearer {os.environ['ISSUED_LIVE_KEY']}"}
api = "https://issued.live/api/v1"
get = lambda path, **kw: requests.get(f"{api}{path}", headers=H, timeout=30, **kw).json()

# 1. The domain record. ssl_cert is the 32-hex cert_id, and it is the bridge.
d = get("/domain/loop-lumen.com")
cert_id = d["record"]["ssl_cert"]
addrs   = [a["ip"] for a in d["addresses"]]

# 2. The certificate. sans is who was issued alongside it; spki_sha256 is the key.
c = get(f"/cert/{cert_id}")
names = set(n.lstrip("*.") for n in c["sans"])
spki  = c.get("spki_sha256")

# 3. Every certificate sharing that key pair. Read coverage before concluding anything.
if spki:
    k = get(f"/spki/{spki}", params={"limit": 500})
    print(k["coverage"])
    for sib in k["certificates"]:
        names.update(n.lstrip("*.") for n in sib.get("sans", []))

# 4. One batch call turns the name set into records, with ns and first_cert_seen.
b = requests.post(f"{api}/domains?include=timing", headers=H, timeout=30,
                  json={"domains": sorted(names)}).json()
ns_seen = {}
for row in b["domains"]:
    if row["status"] != "found":
        continue
    for ns in row["record"].get("ns", []):
        ns_seen[ns] = ns_seen.get(ns, 0) + 1
print(sorted(ns_seen.items(), key=lambda kv: -kv[1])[:5])

# 5. The nameserver pair plus the naming grammar finds the rest of the family.
s = get("/search", params={"name": "loop-*.com", "ns": "ns.cloudflare.com", "limit": 500})
print(s["count"], "names match the grammar on that nameserver")

# 6. And the address, windowed to the hour the first certificate was issued, shows what
#    else was stood up beside it. Serialize these: the gate admits two at a time.
for ip in addrs:
    r = get(f"/ip/{ip}", params={"first_seen_from": c["valid_from"][:10], "limit": 500})
    print(ip, r["count"], "hostnames first seen on or after that date")
```

Each step narrows or widens deliberately: step 3 widens on the strongest link available.
 Step 5 widens on a grammar, which needs the nameserver filter to stay honest. Step 6 dates
 the co-location rather than assuming it.

### Keeping the pivot running

Once you have a nameserver pair and a grammar, the two feeds turn a finished
 investigation into a standing one. Poll `/api/v1/provisioning?ns=` for new names
 on the same infrastructure, and `/api/v1/nrd?certified=1&max_hours_to_cert=1`
 for registrations that took a certificate within the hour.

Store the cursor from each and resume from it. Both feeds are gap-free from a cursor,
 which is what makes a scheduled job safe to miss a run.

## Errors on keyed endpoints

Errors are JSON with an `error` code and a human-readable
 `message`, always sent `Cache-Control: no-store`. Retryable ones also
 carry `"retryable": true` and a `Retry-After`.

| Status | Code | What to do |
|---|---|---|
| 400 | `bad_request` | Malformed input, a range wider than 256 addresses, an unbounded character class, a reversed time window, or a pagination parameter this endpoint does not implement. Fix the input; retrying will not help. |
| 400 | `bad_cursor` | Only on `/api/v1/nrd`: the cursor does not parse, or it is dated in the future. Drop it to restart from the window floor. |
| 400 | `window_too_wide` | On `/api/v1/nrd` and `/api/v1/provisioning`: the window spans more than 13 calendar months. The message names the limit; walk the range in slices. |
| 401 | `unauthorized` | Missing or unrecognized key. Every endpoint on this page can answer it. The [unkeyed endpoints](/developers/api) take no key and never do. |
| 404 | `not_found` | Nothing observed for that domain or certificate. From `/api/v1/cert/` it also covers an id whose detail has passed the 14-day retention horizon. An unmatched key hash is not one of these: `/api/v1/spki/` answers 200 with `count: 0`, because a key we never hashed and a key nobody reused look identical from a 404. |
| 405 | `method_not_allowed` | Use GET, except on `/api/v1/domains`, which is POST. Every endpoint except `/api/v1/ip/` and `/api/v1/range/` also sends an `Allow` header naming the method that works. |
| 413 | `too_large` | Only on `/api/v1/domains`: the body is over 512 KiB, or the connection ended before it was complete. |
| 415 | `bad_request` | Only on `/api/v1/domains`: send `Content-Type: application/json`. |
| 429 | `rate_limited` | Over the per-minute ceiling for your plan (300 on Pro). `Retry-After` carries the seconds until the window rolls. There is no daily cap to exhaust. |
| 503 | `timeout` | The query did not finish in time, or both of your account's concurrency slots were already taken. **Expected on large addresses, wide ranges and unanchored searches.** Branch on `retryable` and `Retry-After` rather than on the status number. Retry once, then narrow the range, the window or the pattern. |
| 500 | `internal_error` | Our side. Retry once, then tell us. |

**The timeout status is 503, and it stays 503.** This origin
 sits behind a CDN that replaces origin-generated gateway statuses with its own error page,
 so a 504 reaches you as a few bytes of edge HTML with our JSON body and headers stripped.
 Measured one day apart on the same code path: at 500 the consumer received our body
 verbatim; at 504 they received 16 bytes of "error code: 504".

## Caps and defaults in one table

| Endpoint | Page default | Page max | Other caps |
|---|---|---|---|
| `/api/v1/ip/{ip}` | 500 | 50000 | NDJSON at 5000 or above. |
| `/api/v1/range/{cidr}` | 500 | 50000 | 256 addresses, an IPv4 /24. |
| `/api/v1/search` | 500 | 1000 | Pattern 128 chars, 8 labels, class count bounded at 64. |
| `POST /api/v1/domains` | - | - | 500 domains, 100 with any `include`, 10 sub-entries each, 512 KiB body. |
| `/api/v1/domain/{domain}` | - | - | 50 addresses, 50 certificates. |
| `/api/v1/cert/{hash}` | - | - | 32 or 64 hex. Detail queryable 14 days; older days live in the archived files. |
| `/api/v1/spki/{sha256}` | 500 | 500 | 64 hex. Coverage begins 2026-09-07. |
| `/api/v1/provisioning` | 500 | 500 | `max_hours_to_cert` clamped to 168; window 13 months. |
| `/api/v1/nrd` | 50 | 500 | Window 13 months. |

Per key: no daily cap, one advanced query in flight on Plus and two on Pro, and 300 (Plus) or 1,200 (Pro) requests a
 minute on the public record API. [Pricing](/pricing) lists what each plan
 reaches.

Last updated September 15, 2026. Start at the
 [documentation hub](/developers) for the other surfaces, or
 [get in touch](/contact) with anything these pages do not answer.

↑ Top

---

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