API keys
A key lets a script or a program search OwlyScan like your AI does.
Create a key
In the app at app.owlyscan.com, use the Connect your AI link on the search screen. It opens the page API keys and AI connection. Create your key there.
- It starts with
owly_. - It is shown once, when you create it. Copy it then.
- Keep it secret: anyone who holds it searches on your quota.
- You can revoke it on the same page.
Two ways in
An Access key works on the search route, on the two routes that reopen a past search, and on the MCP endpoint. Use the routes from a script or a program. Use the MCP endpoint from an AI app: see connect your AI for the URL and the two tools.
Send the key
Send the key in the X-API-Key header, or as a bearer token in Authorization. A missing or invalid key answers 401.
X-API-Key: owly_YOUR_KEY
# or, the same key as a bearer token:
Authorization: Bearer owly_YOUR_KEY Every other route needs the web session of the app. A key does not open them.
Search from a script
Send a POST with a JSON body to /api/search on https://backend.owlyscan.com.
curl -X POST https://backend.owlyscan.com/api/search \
-H "X-API-Key: owly_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "acme.example"}' query: your search, written as in the app. Search syntax. Only the first 20 words are used.start_offset: optional, to page through the results. The default is 0. From 500 on, the answer is a 400AccessLimit: see the cap below.max_hits: optional, the size of a page. If you leave it out, you get 200. A page is clipped to what is left under the cap, which is 500 minusstart_offset, and never refused. Results are grouped, so a page can hold fewer hits thanmax_hits.
The 500-result cap
- Access shows at most 500 results of a search.
darknet.num_hitsstays the real total. - A
start_offsetof 500 or more answers 400 with{"error":"AccessLimit","limit":500}, and nothing is counted.
What comes back
Excerpts only: no full document, no web address and no file name. Read the darknet section and ignore the other sections and the other fields. Here is a shortened answer.
{
"darknet": {
"num_hits": 12,
"hits": [
{
"level0": "ransomware",
"date": "2025-03-14",
"entry": {
"visibility": "deidentified",
"segments": [
{"t": "text", "text": "Invoice sent to "},
{"t": "label", "kind": "person", "n": 1},
{"t": "text", "text": " at "},
{"t": "label", "kind": "email", "n": 1},
{"t": "text", "text": " for "},
{"t": "term", "text": "acme.example"},
{"t": "hidden", "w": 3}
]
}
}
],
"entry": {
"yours_on_page": 0,
"likely_on_page": 0,
"search_id": "0b7c2e4a-5f1d-4c3e-9a8b-1d2e3f4a5b6c",
"shown_limit": 500,
"preparing": 0
}
}
} darknet.num_hits: the total in the index for your query, not the number of hits in the page.darknet.hits: the page. Each hit haslevel0, the channel (for exampleransomware), anddate, as YYYY-MM-DD or an empty string.entry.visibilitysays what you may read.yoursis a match of your company, readable.deidentifiedis a match of another company, with names replaced by labels.fieldsis a match of another company with no link to yours found: field names and your search terms, with at most one ordinary word on each side, are visible; every other value is hidden.hiddenis masked: only your search terms are visible.likelyis a match that probably concerns you, without certainty, or that a search for a brand linked to your company finds: organisation, brand and domain names are text, every other name or number stays a label, and passwords and hashes aresecretsegments. Treat any value you do not know ashidden.entry.segmentsis the excerpt in pieces. Join them to rebuild it:textis plain text,termis one of your search terms,labelis a masked person, email address or other identifier, numbered within the page (kindandn), andhiddenis a masked stretch (wis only a display width).secretis a masked password or hash in your own records. A password hasprefix, at most its first two characters (an empty string when none are shown), andlength. A hash hashash, its type such asMD5or null when unknown, andlength, and noprefix.- Treat any segment type you do not know as
hidden. darknet.entry.yours_on_page: optional, how many of your company’s matches are in this page.darknet.entry.likely_on_page: the number oflikelyresults on this page, 0 when there are none.darknet.entry.search_id: the id of this search, a string. Use it to reopen the search for free: see below.darknet.entry.shown_limit: how many results Access can show for a search, 500.darknet.entry.preparing: how many hits of this page are still being prepared.entry.hidden: set on a hit whose excerpt is not shown.preparingmeans the de-identified excerpt is being prepared.restrictedmeans it is not shown in Access, and it will not change.- Every hit also has
idandtext: ignore them.
Hits being prepared
While darknet.entry.preparing is above 0, ask for the same page again through /page, which is free: see below. Never poll through /api/search. Every 5 seconds is enough. Stop when preparing reaches 0, or after about 2 minutes without a change.
Your own records are shown as they are, the others with names hidden, as in the app. In your records, passwords and hashes are masked: only their length and at most their first two characters show. Privacy and data.
Quota
A key shares the weekly counter with the app and your AI. The same query within 20 minutes of its first search is one search of your 100 a week, whatever the start_offset, and that search is shared with the app and your AI. After 20 minutes, the same query counts again. Reopening a past search never counts. Limits and quota.
Reopen a past search
Two routes reopen the searches you made in the last 90 days. Neither counts as a search.
GET /api/entry/searcheslists your own searches, newest first, at most 100, none older than 90 days, with no paging. The answer is{"searches":[{"id","query","created_at","total","person_query"}],"retention_days":90}.POST /api/entry/searches/SEARCH_ID/pagereopens one of them, withidfrom the list or fromdarknet.entry.search_id. The body is optional:start_offset(default 0) andmax_hits(default 50), with the same 500 cap.
curl -X POST https://backend.owlyscan.com/api/entry/searches/0b7c2e4a-5f1d-4c3e-9a8b-1d2e3f4a5b6c/page \
-H "X-API-Key: owly_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"start_offset": 0, "max_hits": 50}' The answer has the same shape as a search answer, plus darknet.entry.searched_at, in ISO format, UTC. Results are as of that moment: nothing indexed later appears. To see newer records, search again, which counts once the 20 minutes have passed.
Errors
- 401: the key is missing or invalid.
- 400: the query filters on a field other than the text, the date or
level0. It costs nothing. - 400
AccessLimitwithlimit:start_offsetis 500 or more. Nothing is counted. - 402: the subscription is not active.
- 404
SearchNotFound, on/page: the search is unknown, is not yours or is older than 90 days. - 429 with
resets_at: the weekly limit is reached. The answer givesresets_at(UTC),usedandlimit, and aRetry-Afterheader in seconds. - 429 without
resets_at: too many requests in a short time, on any route. Wait a moment and try again. - 503: temporary. Retry after a few seconds.
Connected AI apps
The same page lists the AI apps you connected, such as Claude, under Connected AI apps. Disconnect takes effect immediately.