Skip to content

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:

{
  "result": {
    "sourceId": 12,
    "created": [101, 102],
    "updated": [7],
    "invalid": ["not-a-value"]
  }
}

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.