Skip to content
DocumentationAPI ReferenceChangelog
API ReferenceTalon Studio v1.0.2-beta

Errors & rate limits

A consistent error envelope, standard HTTP status codes, rate-limit headers on successful responses, and how each list endpoint pages.

Error format

Every error returns a JSON body with a human-readable message. Validation failures (422) also include a field-keyed errors map:

422 Validation failed
{
"message": "Validation failed",
"errors": { "streamId": ["Required"] }
}
Keys are dotted field paths (sourceIds.0). A rule that spans fields — "either sigma or spl is required" — and a missing or non-JSON body are reported under the key (root).
When an org-scoped token omits workspaceId, the 400 reads A workspaceId query parameter is required even on POST/PATCH endpoints — there, put workspaceId in the body.

Status codes

200 · 201 · 202
Success. 201 when a POST creates something (a draft change request, a case, a card); 202 when PATCH /detections/{id} stages an edit for review.
400
Bad request — an org-scoped token omitted workspaceId, or input passed schema validation but failed a later check: a proposed detection that fails detection validation, an invalid dataset name, a search lookback over 7 days, a workboard column from another workspace.
401
Missing or invalid token — no Bearer header, an unknown, revoked or expired token, a disabled owner, or a personal token whose owner no longer exists.
403
Forbidden — the token lacks the required scope; a write scope without an Enterprise licence; the IP allowlist rejected the caller; a workspace-bound token asked for another workspace or called an org-wide endpoint; a service token tried to propose a detection; the MCP server without an Enterprise licence. On an instance with no Talon key, or one whose stored key no longer verifies, every call is refused before its handler runs with 403 and the code licence_required — the most likely first error on a fresh install; an admin pasting the Community key clears it.
404
Not found — the resource doesn't exist or isn't in the token's reach (a uniform 404, so ids can't be probed). Also returned when no search source has search access enabled. Unknown adversaries and ATT&CK techniques get "Did you mean" suggestions in the message.
409
Conflict — promoting an alert that is no longer a candidate, or a detection edit that would rename or re-map the detection's file.
422
Validation failed — see the errors map for the offending fields.
429
Rate limit exceeded — back off and retry after Retry-After.
500
Server error.
503
PATCH /detections/{id} changed the Sigma but the Sigma translation sidecar is unavailable, so the SIEM queries couldn't be re-derived. Retry once it is running.

Rate limits

Requests are limited per token (120 requests a minute by default) behind a per-IP backstop (600 a minute). Successful responses (and any 429) carry the current per-token budget. Error responses such as 403 or 422 do not include these headers, and neither do GET /coverage/navigator-layer, which returns a raw Navigator file, or the MCP server.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1719763200

X-RateLimit-Reset is the Unix time, in seconds, when the window ends.

PUBLIC_API_RATE_LIMIT
Requests per token per minute. Default 120.
PUBLIC_API_IP_RATE_LIMIT
Requests per source IP per minute, checked before the token is looked up. Default 600.
PUBLIC_API_SEARCH_RATE_LIMIT
Extra per-token budget for POST /search (and the MCP execute_search tool), on top of the per-token limit. Default 20.
DISABLE_RATE_LIMIT
Set to true to turn all of the above off. Private labs only.
TALON_TRUST_PROXY
Set to true (or 1) when a trusted reverse proxy sets X-Real-IP or X-Forwarded-For, so the per-IP limit and token IP allowlists see real client IPs.
The window is a fixed 60 seconds and isn't configurable — only the counts are. A value of 0, a negative number or a non-number is ignored and the default applies; use DISABLE_RATE_LIMIT to switch limiting off.
Without TALON_TRUST_PROXY, the per-IP backstop collapses to one shared bucket for all callers — the 600-a-minute budget applies to every client combined, not to each one.
The per-IP backstop counts every request. The per-token budget is only spent once the token has passed its scope and licence checks, so a 403 doesn't consume it.
The limiter is in-process memory, so a multi-replica deployment enforces an independent budget per replica, and a restart resets the counters.

When you exceed a limit the API returns 429 with a Retry-After header — wait that many seconds before retrying.

The pre-auth 429 doesn't match this shape
The per-IP backstop (checked before the token is even resolved) returns { "error": "Too many requests" } instead of { "message": "Rate limit exceeded" }. The Retry-After and X-RateLimit-* headers are present on both. Handle both body shapes in a client.
One budget per token
The per-token window is keyed on the token's id, so every client sharing a token shares its 120 requests a minute. Create a separate token per integration — each gets its own budget, and revocation stays surgical. On the Community Edition that means at most two integrations.

Pagination

GET /detections uses offset pagination — pass page and perPage (1–100), and read the returned meta and links:

200 OK
{
"data": [ … ],
"meta": { "page": 1, "perPage": 25, "total": 214, "totalPages": 9 },
"links": { "next": "/api/v1/detections?page=2&perPage=25", "prev": null }
}
Invalid values aren't rejected: a missing or invalid page becomes 1, a missing or invalid perPage becomes 25, and a perPage over 100 is clamped to 100. A page past the end returns an empty data array.
links are paths relative to your host, and they carry only page and perPage. If you filtered with workspaceId, add it back when you follow a link, or the next page is unfiltered.
The list is capped at the 500 most recently updated detections before paging, so total never exceeds 500.

GET /audit-logs uses a cursor instead — see Audit. The other list endpoints don't page; they return everything or a fixed number of rows:

EndpointOrderRows returned
GET /audit-logsNewest firstlimit 1–2,000 (default 200); page with cursor
GET /agent-runsNewest firstlimit 1–200 (default 50)
GET /coverage/gapsMost-used by adversaries firstlimit 1–100 (default 25)
GET /alertsNewest occurrence firstAt most 100
GET /casesNewest firstAt most 100
GET /notificationsBy nameAt most 100
GET /change-requestsMost recently updated firstAll, in every status
GET /workspacesNewest firstAll in reach
GET /usersOldest account firstAll active users
Everything else—All

Where a limit is accepted, a missing or invalid value falls back to the default and a value over the maximum is clamped to it, rather than returning a 422.