API Reference¶
Base URL: https://<host>/api/v1. Authentication and ACL: see Tokens & ACL. The Web GUI uses the same API with a session cookie instead of a token. The ACL column below shows the path a token's patterns are matched against.
API browser: https://<host>/api/ – interactive reference (Swagger UI) with “Try it out”, using a token (Authorize) or your GUI session; the OpenAPI description is at /api/openapi.json – since v1.31.0.
Conventions¶
JSON envelope – every endpoint except exports returns:
{
"result": {},
"success": true,
"errors": [],
"messages": [],
"result_info": { "page": 1, "per_page": 20, "count": 2, "total_count": 2 }
}
result_info only on lists. Errors: result: null, success: false, errors: [{ "code": 404, "message": "Source not found." }].
Exports return plain text; their errors are plain text too.
Pagination (lists): page (default 1), per_page (5–100, default 20), direction (asc/desc by ID, default desc).
Status codes
| Code | When |
|---|---|
| 400 | Invalid parameter or body, model validation failed (message lists the reasons) |
| 401 | Missing or invalid token; expired token or a sub-token of a revoked one (v1.40.0+) |
| 304 | Export unchanged (If-None-Match matched its ETag) |
| 429 | Too many failed sign-ins from this IP, or too many exports for this token (Retry-After) |
| 403 | ACL denies the path; the token's IP filters refuse the client; protected primary admin token |
| 404 | Unknown resource (source, indicator, group, token) |
| 404 | Also for unknown /api/v1/… paths (JSON) |
| 409 | Unique name already taken |
| 500 | Server error (reported to Sentry) |
GUI sessions¶
Used by the Web GUI; no token needed.
| Method & path | Description |
|---|---|
GET /auth/config |
{ "oidc": bool, "password": true } – offered sign-in methods. |
POST /auth/login |
{ "username", "password" } = name and secret of an admin token; sets the session cookie. 401 on bad credentials or an expired / revoked token, 403 from a refused IP. |
GET /auth/oidc/start |
Redirects to Authentik (Authorization Code + PKCE). |
GET /auth/oidc/callback |
Authentik's redirect target; sets the session cookie and redirects to /app/. |
GET /auth/me |
The signed-in user, or 401. |
POST /auth/logout |
Ends the session. |
With a session cookie, state-changing requests must send X-TIS-Client: web (otherwise 403).
Hits¶
Since v1.33.0. The machines that block with the lists report how often a listed value blocked something. Counts add up per token, reporter (the machine name), value and hour; the nightly job sums hours older than CONFIG_STATS_HOURLY_DAYS up per day and deletes what is older than CONFIG_STATS_RETENTION_DAYS. Reports are neither audited nor clear the export cache.
| Method & path | ACL | Description |
|---|---|---|
POST /hits |
hits |
Report: JSON { reporter, at, hits: [{ value, count }] } (≤ 10,000 values) or text/plain "value count" per line with ?reporter=&at=. → { counted, invalid, bucket }. API token only. |
GET /hits/top |
hits/top |
Most hit values with the live sources listing them. from, to (default the last 7 days), reporter, tokenId, limit (≤ 500). |
GET /hits/timeline |
hits/timeline |
Hits per interval (hour, day), optionally byReporter=true, or by=type / by=source (v1.40.0+; key = the type or source ID, null when no source lists the value; a value listed by several sources counts for each). Same filters. |
GET /hits/reporters |
hits/reporters |
Reporting machines: token, reporter, hits, distinct values, last report. |
Since v1.43.0, listedOnly: true (JSON) or ?listed_only=true (text/plain) counts only values a live indicator lists; the rest is returned in unlisted – for reporters that see more than the lists block, such as fail2ban's bans.
Values are normalised like indicators (Evil.Example. → evil.example, 10.0.0.0/8). GET /indicators returns hits (last 7 days) and hitsPrevious (the 7 days before, v1.40.0+) per indicator and sorts by hits (sort=hits); the Dashboard shows the most hit values, a chart and the reporting devices.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: text/plain" \
--data-binary $'evil.example 42\n192.0.2.10 7' "https://api.scrany.net/v1/hits?reporter=$(hostname)"
Devices¶
Since v1.41.0. tis-agent (v0.5.0+) checks in every minute with its state; the platform keeps the latest check-in per token and reporter – a device – and the changes between check-ins for CONFIG_AGENTS_EVENT_RETENTION_DAYS. A device without a check-in for CONFIG_AGENTS_OFFLINE_AFTER seconds turns offline, a job with CONFIG_AGENTS_FAILING_AFTER failed runs in a row is failing; both are logged and sent to Sentry, and the web GUI shows them on the Devices page and the Dashboard. Check-ins are neither audited nor clear the export cache.
| Method & path | ACL | Description |
|---|---|---|
POST /agent/checkin |
agent/checkin |
JSON { reporter, agent: { version, started_at }, host: { hostname, os, release, arch, uptime }, config: { version }, targets: [{ name, type }], jobs: [{ name, kind, target, list, source, every, last_run, last_change, next_run, result, error, failures, entries, etag }], hits: { mode } }; unknown fields are ignored. → { interval } (seconds until the next check-in). API token only. |
GET /devices |
devices |
All devices, the most recently seen first: token (with its parent), status (online, offline), the last check-in, failingJobs and hits (last7Days, lastReport). |
GET /devices/{id} |
devices/{id} |
One device with its latest 200 events (new, online, offline, version, config, failing, recovered). |
DELETE /devices/{id} |
devices/{id} |
Forget a device and its events (audited); it comes back with its next check-in. |
agent/* is in the default ACL of the guest and user roles; reading devices is for admins.
Central configuration¶
Since v1.42.0, with tis-agent v0.6.0+ and "managed": true. The platform assigns pull jobs to managed devices: a template on a top-level token (for every device of it and its sub-tokens) and a device's own jobs, which override a template job of the same name, switch it off ({ "name": "dns", "disabled": true }) or add to it. A job is a name, every (at least 1m) and pull: list or sources, target (the name of one of the device's targets), format, type, tag, whitelist, country, exclude_country, rpz_ip, allow_empty – never a command, a path or a URL. Jobs that don't fit the targets the device reported (unknown target, a format or type the target type can't take, a target used twice) are left out and shown with the reason.
The check-in sends managed, config.central_version (local = none), config.central_error and commands: [{ id, result, message }] (acknowledgements); the answer adds config: { version, jobs } when the version differs (jobs: null = no central jobs, run the local ones) and commands: [{ id, type: "pull" | "report-now", job? }], delivered with every check-in until acknowledged, for an hour at most.
| Method & path | ACL | Description |
|---|---|---|
GET /tokens/{id}/agent-jobs |
tokens/{id}/agent-jobs |
The template of a token. |
PUT /tokens/{id}/agent-jobs |
tokens/{id}/agent-jobs |
Replace it: { jobs: [...] }. Top-level tokens only (a sub-token's devices use the parent's). |
GET /devices/{id}/agent-jobs |
devices/{id}/agent-jobs |
The device's own jobs. |
PUT /devices/{id}/agent-jobs |
devices/{id}/agent-jobs |
Replace them: { jobs: [...] }, entries with disabled: true switch template jobs off. |
GET /devices/{id}/config |
devices/{id}/config |
The effective jobs with origin (template, device, override) and problem, the jobs switched off, the version sent, whether the device runs it (current) and centralError. |
POST /devices/{id}/commands |
devices/{id}/commands |
{ type: "pull", job? } (all pull jobs without job) or { type: "report-now" }; managed devices only. |
GET /devices/{id}/commands |
devices/{id}/commands |
The latest commands with their state (pending, delivered, ok, error, expired) and message. |
Changes are audited; the device's history shows config-applied, config-refused and command events, and a refused configuration is also sent to Sentry.
Audit log¶
| Method & path | ACL | Description |
|---|---|---|
GET /audit |
audit (admin) |
Newest first, paginated. Filters: actor, actorType (token, session, cli), action (prefix, e.g. DELETE), target and q (substrings), from/to (date-time). Sort: sort = createdAt, actor, actorType, action, target, ip; direction = asc/desc. |
GET /audit/facets |
audit/facets (admin) |
{ "actors": [...], "actions": [...] } – distinct values for filters. |
Every successful change through the API (and so the GUI) is recorded with actor, action (PUT /sources/:id), target, sanitised body and client IP; changing CLI commands too. Passwords, tokens and source header values are never stored.
Health¶
| Method & path | ACL | Description |
|---|---|---|
GET /healthz (outside /api/v1) |
public | Database and Redis check; 200 ok/degraded, 503 error. |
Stats¶
| Method & path | ACL | Description |
|---|---|---|
GET /stats |
public | { "indicators": 12000 } – rounded indicator count. |
GET /stats/summary |
stats/summary |
Live indicators per source and type (the dashboard); delta = the net change (added − removed) over the last days (default 7, v1.40.0+), per source and in total. |
GET /stats/analytics |
stats/analytics |
Since v1.40.0. The Analytics data: indicators added, removed and live, hits and audit entries per UTC day over days (7, 14, 30, 90, 180, 365; default 30), with the totals of the period before; top sources, tags and countries; the sources table. breakdown=type / source adds a field per category to every day (live_fqdn, created_s5, hits_other, listed in categories). sourceId, type, tag, country or value narrow everything to one thing. |
Export¶
| Method & path | ACL | Description |
|---|---|---|
GET /export |
export |
Public export (non-private sources). Cached. |
GET /export/:ref |
export/<id, name or uuid> |
One source by ID, name or UUID (v1.40.0+), incl. private. |
GET /export/private/:refs |
export/private/<id, name or uuid> each |
Several sources (5,google-ips), incl. private. 404 lists missing ones. |
GET /export/group/:ref |
export/group/<id, name or uuid> |
A group by ID, name or UUID. |
Query: format (plain, hosts, rpz, zone), type, tag, whitelist. Details in Export.
Import¶
| Method & path | ACL | Description |
|---|---|---|
POST /import/:ref |
import/<id, name or uuid> |
Body = raw feed text (≤ 10 MB) parsed with the source's format and options. ?tag=a,b adds tags to every imported indicator. The source by ID, or since v1.40.0 also by name or UUID; a token without access gets 403 also for unknown sources. |
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: text/plain" \
--data-binary @list.txt "https://api.scrany.net/v1/import/5?tag=honeypot"
{
"result": {
"sourceId": 5,
"sourceName": "internal-honeypot",
"totalParsed": 3,
"created": 2,
"updated": 1,
"skipped": 0
}
}
Sources¶
| Method & path | ACL | Description |
|---|---|---|
GET /sources |
sources |
List (paginated) with tags. |
POST /sources |
sources |
Create. 201; 400 for an invalid cron schedule (also on update; since v1.30.0). |
GET /sources/schedule?schedule= |
sources/schedule |
Checks a cron expression or preset (&name= picks a preset's time): { valid, cron, nextRun, timeZone, presets } (server time zone) – since v1.30.0. |
GET /sources/defaults |
sources/defaults |
{ daysRetention, daysPurge } used by sources without their own – since v1.32.0. |
GET /sources/:id |
sources/:id |
Detail with tags. |
PUT /sources/:id |
sources/:id |
Partial update; tags replaces the tag set. |
DELETE /sources/:id |
sources/:id |
Soft-delete with its indicators. 204. |
POST /sources/:id/fetch |
sources/:id/fetch |
Fetch now; returns the ingest summary plus warning for a suspiciously small result; the outcome is recorded on the source (fetch status). 400 for sources without URL and manual sources. |
Source fields: name (slug, required), description (v1.40.0+), url, format (plain, hosts, json, csv; default plain), schedule (cron or a preset like @every 6h, needs url), daysRetention / daysPurge (own periods, null = default; since v1.32.0), options (object, see Sources), private (bool), manual (bool, no url/schedule), tags (array or comma-separated). Responses also carry uuid (v1.40.0+, read-only) and the fetch status.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"premium-feed","url":"https://api.example.com/malware.txt","schedule":"0 * * * *","tags":["malware"],"options":{"timeout":10000}}' \
https://api.scrany.net/v1/sources
Indicators¶
| Method & path | ACL | Description |
|---|---|---|
GET /indicators |
indicators |
List (paginated). Filters: type, tag (indicator tags, comma-separated: any of them, or all with tagMatch=all – since v1.33.0), sourceId, permanent=true/false, country (GeoIP codes, comma-separated – since v1.39.0), q (substring). Sort: sort = id (default), value, type, country, source, lastObservedAt, permanent – since v1.30.0. |
POST /indicators/bulk |
indicators/bulk |
Several at once – ids (1–1000) or filter (the list filters: type, sourceId, tag, tagMatch, permanent, country, q) and action: delete, or update with permanent, addTags, removeTags → { action, affected } – since v1.33.0. |
GET /indicators/countries |
indicators/countries |
GeoIP countries of the indicators with counts ([{ code, count }], most common first) – since v1.39.0. |
GET /indicators/tags |
indicators/tags |
Names of the tags in use on indicators – since v1.30.0. |
POST /indicators |
indicators |
Add values by hand – since v1.25.0. |
GET /indicators/lookup?value= |
indicators/lookup |
Is a value blocked? Matches for an IP/CIDR (incl. containing networks), domain (incl. parent domains) or URL, with source, tags, match kind and GeoIP country (also of the looked-up IP in query.country). |
GET /indicators/:id |
indicators/:id |
Detail with tags. |
PUT /indicators/:id |
indicators/:id |
tags (replace) and/or permanent (bool). |
DELETE /indicators/:id |
indicators/:id |
Soft-delete. 204. |
POST /indicators body: sourceId (required), values (array) or value (string), tags, permanent (bool). Response 201 if anything was created, else 200:
Groups¶
Since v1.25.0. :ref is the group ID, name or (v1.40.0+) UUID; groups carry uuid. The GUI calls them lists.
| Method & path | ACL | Description |
|---|---|---|
GET /groups |
groups |
List (paginated) with sources, whitelistSources and blacklistSources (each { id, name, private, tags, excludeTags }). |
POST /groups |
groups |
Create: name, description, sources, whitelist, blacklist, whitelistSources, blacklistSources. Source lists take refs (urlhaus[phishing,!test,<8h]) or { source, tags, excludeTags, maxAge }. 201. |
GET /groups/:ref |
groups/:ref |
Detail. |
PUT /groups/:ref |
groups/:ref |
Partial update: name, description; replace sources/whitelist/blacklist/whitelistSources/blacklistSources, or edit with add…/remove… (e.g. addSources, removeWhitelist, addBlacklistSources). |
DELETE /groups/:ref |
groups/:ref |
Delete (sources stay). 204. |
Unknown sources → 404 (Source(s) not found: …), invalid list entries → 400.
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"addSources":["urlhaus-hosts"],"addWhitelist":["cdn.example.com"]}' \
https://api.scrany.net/v1/groups/customer-x
Tokens¶
| Method & path | ACL | Description |
|---|---|---|
GET /tokens |
tokens |
List (paginated); secrets/hashes never returned. |
POST /tokens |
tokens |
Create: name, description, role (default guest), acl, and since v1.40.0 expiresAt (date-time or 30d), allowedIps, deniedIps, parentId (a sub-token – role/acl are the parent's). Response includes plainToken – shown once. |
GET /tokens/:id |
tokens/:id |
Detail. |
PUT /tokens/:id |
tokens/:id |
Update name, description, role, acl, expiresAt, allowedIps, deniedIps. Primary admin token: role and restrictive ACL changes → 403; a sub-token's role or ACL → 400. |
DELETE /tokens/:id |
tokens/:id |
Delete, together with its sub-tokens. Token 1 → 403. |
POST /tokens/:id/rotate |
tokens/:id/rotate |
New secret, returned once as plainToken. |