Skip to main content
The API exposes the same search and app records as the console, as JSON. To work with them from an AI assistant or agent instead, connect the MCP server.

Endpoints

Both are described in full, with a playground, under Endpoints in the sidebar. /v1/apps (or /v1/apps/) takes GET only; any other method, HEAD included, answers 405. Any other path under /v1 answers 404; see Limits and errors. Every response, errors included, is JSON with Content-Type: application/json and Cache-Control: no-store.

A first call

The first hit has a full snapshot. The second was visited and answered 401, but its page has not been captured yet, so it has no snapshot links, no emails and no verifiedDetectors.

The response

Every key is always present. q is required. A missing or blank q answers 400 and uses no search:
A q that reads as nothing, such as status:2oo alone, returns an empty page whose query is "", showing that the filter was dropped, and uses no search:

App fields

A search hit and an app record share the same fields, with the same values for the same hostname. The app record adds text and credentials. The rules behind the table: strings, numbers and booleans are always present. A timestamp is omitted when the event never happened. A list is [] when Periphery looked and found nothing, and omitted when it never looked. Signed links and faviconHash are omitted when there is nothing to link to. No field is ever null. scanned: false means Periphery has visited the host but holds no snapshot of its page yet: every render so far failed or hit a bot wall. Its status, if any, comes from a fallback request. Such an app has no snapshot links and no lists, and its title and text are empty.
Credential findings only ever come masked, in credentials[].masked, and the raw page source the credential scan ran over is never served. The text field and the screenshot show what the page displayed when it was captured, so a secret the page printed on screen appears in them as shown. Treat responses as sensitive. See Emails and credentials.
faviconUrl and screenshotUrl are signed and expire after an hour: download what you want to keep. To refresh the links of one app, call GET /v1/apps/{hostname}: it returns fresh links without using a search, though it shares the one-request-per-second budget.

Pagination

Search results come in pages of 50. Request another page with page:
Keep requesting the next page while hasMore is true, and stop when it is false. hasMore is exact: it is true only when the next page has at least one hit. A page past the end returns an empty hits array. A page that is not a number, or is below 1, is read as 1. Page 20,000 is the last one, and its hasMore is always false; a larger page answers 400 page_out_of_range. Each page is one search against your monthly allowance, and the per-second rate limit applies. Pages up to 20,000 are allowed, but 3,000 searches a month read at most 3,000 pages, or 150,000 hits; narrow the query to reach what you need in fewer pages. total comes from a count that Periphery keeps for up to 10 minutes, raised to at least what the page you are reading proves. Treat it as an estimate for progress bars, not as a stop rule. Counting stops at 1,000,000: past that, total reads 1000000 and totalCapped is true. Order. A query with free text is ordered by relevance. A query of filters only is ordered by firstSeen, newest first, then by lastSeen, newest first; apps with no firstSeen come last. Pages are offsets over live data. Apps enter and leave the index between your requests, so one app can appear on two pages, or move past you between them. De-duplicate by hostname. Each page is cached for up to a minute, and total for up to 10 minutes. GET /v1/apps/{hostname} reads the record live: when it disagrees with a search hit, the app record is the current one.

The query field

query shows the query Periphery actually ran, in the canonical form of the query syntax. Filter values it could not read are dropped or searched as text, so compare it with what you sent to catch a typo:
Here status:abc was dropped, acme.com became a domain search, and 7d was written as 1w. A value searched as text comes back as you sent it, with the free text at the start of query, so it can read like a filter. Filters follow the free text in a fixed order, and lastSeen: and firstSeen: windows come last. A window that applies always has < or >, a whole number of at least 1 and a unit of h, d or w, and is at most 15,250 weeks; a lastSeen: or firstSeen: token without those, or followed by anything other than another window, was searched as text:

Credentials in responses

Every API key belongs to a Plus account, so responses carry what Plus sees: email addresses on search hits and app records, and on an app record the credentials array with every finding, verified or not, each value masked. hasVerifiedCreds and verifiedDetectors summarise it. The has:creds filter in q always applies. See Emails and credentials.

Versioning and changes

The /v1 contract changes in two ways:
  • Additive changes ship at any time: new fields, new error codes, and new values in open lists such as provider slugs. Ignore fields and values you do not know rather than rejecting the response.
  • Removals, renames and changes of meaning are announced at least 90 days ahead, in the changelog and by email to every account that holds an API key.
The changes of 5 November 2026 shipped without notice because the API had not launched yet.

Playground

Each endpoint page has a playground that sends real requests with the key you paste. They are rate-limited like any other request, and each search uses one of your monthly searches.

Common automation patterns

See API recipes for pagination, retrieving full records, handling rate limits and errors, and keeping credential-bearing responses out of logs and third-party tooling.