API endpoints
Version 0.1.7, OpenAPI 3.1.0, 131 operations across 20 groups.
Everything below /api/v1 requires an authenticated session except where a
group says otherwise. The full machine-readable contract is served by a running Sift at /api/openapi.json, with Swagger UI at /api/docs, only while SIFT_ENVIRONMENT is development.
Contents
Section titled “Contents”- health — 2 operations
- auth — 4 operations
- system — 8 operations
- admin — 6 operations
- credentials — 11 operations
- kindo — 5 operations
- analysis — 5 operations
- copilot — 10 operations
- filters — 37 operations
- gold-set — 4 operations
- models — 6 operations
- operations — 7 operations
- activity — 5 operations
- alerts — 1 operations
- dashboard — 3 operations
- fleet — 1 operations
- labels — 3 operations
- recipes — 2 operations
- verdict-check — 4 operations
- webhooks — 7 operations
health
Section titled “health”Unauthenticated. /healthz is liveness, /readyz checks Postgres, Redis, and object storage and is the one an orchestrator should gate on.
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /healthz | public | Healthz |
| GET | /readyz | public | Readyz |
GET /healthz
Section titled “GET /healthz”Requires: public
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | HealthResponse |
GET /readyz
Section titled “GET /readyz”Requires: public
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ReadyResponse |
Login issues a JWT in an httpOnly SameSite=Lax cookie carrying a bound CSRF nonce. Every unsafe method must echo that nonce in X-CSRF-Token.
| Method | Path | Requires | Summary |
|---|---|---|---|
| POST | /api/v1/auth/change-password | admin, operator, reviewer | Replace the current password. |
| POST | /api/v1/auth/login | public | Exchange credentials for a session cookie. |
| POST | /api/v1/auth/logout | admin, operator, reviewer | End the session on the server, not only in this browser. |
| GET | /api/v1/auth/me | admin, operator, reviewer | The current session. |
POST /api/v1/auth/change-password
Section titled “POST /api/v1/auth/change-password”Replace the current password.
Clearing the seeded default is what removes the standing system warning, so this is the remediation the console’s banner points at.
A new session is issued on success. Every earlier session of the account ends, on every device, because the change moves the account’s session epoch on: a session opened with the old password must not survive the password.
A wrong current password is a guess at the account’s password, and it counts against the same per-username budget as a failed sign-in, so a stolen session cannot guess here without limit. Past that budget the answer is 429, whichever form spent it and whether or not this guess is right.
Requires: admin, operator, reviewer
Body: ChangePasswordRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SessionResponse |
422 | Validation Error | HTTPValidationError |
POST /api/v1/auth/login
Section titled “POST /api/v1/auth/login”Exchange credentials for a session cookie.
The session is httpOnly and never appears in a response body. The CSRF token does, because the client has to echo it back on every state-changing request; it is useless without the cookie.
Requires: public
Body: LoginRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SessionResponse |
422 | Validation Error | HTTPValidationError |
POST /api/v1/auth/logout
Section titled “POST /api/v1/auth/logout”End the session on the server, not only in this browser.
Clearing the cookie used to be all this did, so a copy of it taken beforehand stayed valid until it expired. Moving the account’s session epoch on ends that copy, and with it every other session of the account: signing out here signs out everywhere.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Ack |
GET /api/v1/auth/me
Section titled “GET /api/v1/auth/me”The current session.
Returns the CSRF token as well as the user so a reloaded page can recover it without parsing cookies or signing in again. The user is read from the database on every call, so a role change or a cleared default-password flag is reflected immediately.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SessionResponse |
system
Section titled “system”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/system/decisions | admin, operator, reviewer | The decision model this deployment asks, whether it is answering, and its usage. |
| POST | /api/v1/system/decisions/ping | admin | Ask the decision model one fixed question now, and say what came back. |
| GET | /api/v1/system/kindo | admin, operator, reviewer | Whether Sift is reaching the Kindo tenant, from the calls it already makes. |
| POST | /api/v1/system/ping | admin, operator | Dispatch a task and wait for its result. |
| GET | /api/v1/system/recipes | admin, operator, reviewer | Which recipes this deployment can build a filter on, and why not where it cannot. |
| GET | /api/v1/system/slots | admin, operator, reviewer | The pipeline’s slots, in order, and which recipes this deployment can run in each. |
| GET | /api/v1/system/spec-rules | admin, operator, reviewer | Which way each spec change needs approval, before anything is saved. |
| GET | /api/v1/system/status | admin, operator, reviewer | Service identity plus anything an operator should act on. |
GET /api/v1/system/decisions
Section titled “GET /api/v1/system/decisions”The decision model this deployment asks, whether it is answering, and its usage.
Every provider Sift knows, in the order it prefers them (Kindo, Jev, a local Ollama in development), with the one in use, its availability through the circuit breaker, and calls and tokens by feature for today and the last 30 days, with an estimated cost at the provider’s list price.
Read from settings, Redis and the usage table: no upstream call, like /system/kindo. POST /system/decisions/ping is the check that calls out.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | DecisionModelStatus |
POST /api/v1/system/decisions/ping
Section titled “POST /api/v1/system/decisions/ping”Ask the decision model one fixed question now, and say what came back.
An admin’s Check now. It goes through an open breaker - it is the trial a person asked for, and an answer closes the breaker for every process - bypasses the answer cache, and is metered as the check feature, so it costs what it says. A model that does not answer is a result here (answered: false with the reason), not an error: saying so is what the check is for.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | DecisionPing |
GET /api/v1/system/kindo
Section titled “GET /api/v1/system/kindo”Whether Sift is reaching the Kindo tenant, from the calls it already makes.
Its own endpoint rather than more fields on /system/status, so the five-minute status poll stays cheap and the console can refresh the connection on the cadence a dashboard indicator wants.
Deliberately makes no upstream call of its own. A panel that probed somebody’s control plane because a tab was left open is a load generator, not a diagnostic - the same reason the reachability check on the credentials screen is a button.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | KindoConnection |
POST /api/v1/system/ping
Section titled “POST /api/v1/system/ping”Dispatch a task and wait for its result.
Synchronous by design. This is a diagnostic that has to fail loudly when the broker or a worker pool is down, which is exactly what a fire-and-forget dispatch would hide.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
queue | query | no | io | ml | notify | Default io. |
echo | query | no | string | Default pong. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PingResult |
422 | Validation Error | HTTPValidationError |
GET /api/v1/system/recipes
Section titled “GET /api/v1/system/recipes”Which recipes this deployment can build a filter on, and why not where it cannot.
The runtime half of the recipe catalog, and since the deployment’s own answer beside it. The console’s copy of the catalog is generated from the build, so it can say a recipe is offered and cannot say whether this image can train it - and SetFit is offered by every build and needs an optional extra the default image does not carry, plus weights only that variant stages. Nor can it know what an administrator has turned off. Authoring refuses on the same three answers, so a screen that reads this offers nothing the server will turn down.
trainable and enabled stay separate fields rather than one verdict, because they gate different things: training asks only the first, so a filter already deciding with a recipe an administrator turned off keeps training. selectable is the conjunction, for the one question a screen offering new work has.
Asked of the API process, which answers for the workers because every backend role runs one image; training asks again on the worker. Cheap - a module lookup, a few file checks and one small query, never an import of the training framework - so authoring screens can read it freely.
Each row carries its recipe’s params_schema: what the server validates a stage’s params against, with the x-sift annotations a settings form is built from. Served here rather than only in the generated catalog so a console built against an older catalog still draws the bounds this server enforces.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | RecipeAvailabilityReport |
GET /api/v1/system/slots
Section titled “GET /api/v1/system/slots”The pipeline’s slots, in order, and which recipes this deployment can run in each.
Every slot is served, the unbuilt ones included, so a screen draws them as not available in this build rather than leaving a gap nobody can explain. Each recipe is answered per slot, with the same three answers GET /system/recipes gives per recipe: the build, the install, the deployment.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SlotsReport |
GET /api/v1/system/spec-rules
Section titled “GET /api/v1/system/spec-rules”Which way each spec change needs approval, before anything is saved.
POST /filters/{id}/versions/preview classifies a change once it is made; a control that waits for that to say needs approval is drawn after the operator has already moved it. This is the same table, published: every classified path with its rule, and for each slot every kind of change to its stage under that slot’s own rule. From sift.services.spec_changes, so the console restates none of it.
Static for the build, and read by every signed-in role, as the slot registry is.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SpecRules |
GET /api/v1/system/status
Section titled “GET /api/v1/system/status”Service identity plus anything an operator should act on.
Warnings live here rather than only in a log because the failure they guard against is nobody looking. The default-credential warning is the reason the field exists, and it is derived from the account’s actual state rather than from configuration - so it clears when the password is genuinely changed, and not before. The warnings themselves are sift.services.system_status, which the alert evaluator reads too.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SystemStatus |
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/audit-log | admin | Every state transition, with actor and rationale. |
| GET | /api/v1/users | admin | List Users |
| POST | /api/v1/users | admin | Create User |
| POST | /api/v1/users/{user_id}/disable | admin | Stop an account signing in, and end every session it has open. |
| POST | /api/v1/users/{user_id}/enable | admin | Let a disabled account sign in again, with its existing password. |
| POST | /api/v1/users/{user_id}/role | admin | Give an account a different role. |
GET /api/v1/audit-log
Section titled “GET /api/v1/audit-log”Every state transition, with actor and rationale.
Reverse chronological. The cursor pairs timestamp with row id because audit rows written in the same transaction share a timestamp, and paging on time alone would drop or repeat them at a page boundary.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
actor_id | query | no | string, nullable | |
target_type | query | no | string, nullable | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_AuditEntry_ |
422 | Validation Error | HTTPValidationError |
GET /api/v1/users
Section titled “GET /api/v1/users”Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of UserSummary |
POST /api/v1/users
Section titled “POST /api/v1/users”Requires: admin
Body: CreateUserRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | UserSummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/users/{user_id}/disable
Section titled “POST /api/v1/users/{user_id}/disable”Stop an account signing in, and end every session it has open.
Its audit history is kept, and it can be re-enabled. Refused for your own account, and for the last active admin.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
user_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | UserSummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/users/{user_id}/enable
Section titled “POST /api/v1/users/{user_id}/enable”Let a disabled account sign in again, with its existing password.
It comes back with no session: whatever it had open when it was disabled stays ended.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
user_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | UserSummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/users/{user_id}/role
Section titled “POST /api/v1/users/{user_id}/role”Give an account a different role.
Every session the account has open ends, on every device, and it signs in again with the new role. Refused for your own account, and for the last active admin. Asking for the role it already has changes nothing.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
user_id | path | yes | string |
Body: ChangeRoleRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | UserSummary |
422 | Validation Error | HTTPValidationError |
credentials
Section titled “credentials”The stored Kindo API key is an authenticated path into your control plane. No endpoint here ever returns it; responses carry a 4-character suffix only.
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/credentials | admin | The live connection and the key staged beside it; either may be null. |
| GET | /api/v1/credentials/checks/{check_id} | admin | One check of either key, as far as it has got. Every 202 here names this. |
| POST | /api/v1/credentials/pending | admin | Wizard step 2: connect with a new key and stage it, or refuse it with Kindo’s reason. |
| DELETE | /api/v1/credentials/pending | admin | Cancel: delete the staged key and its checks. The live connection is untouched. |
| GET | /api/v1/credentials/pending/catalog | admin | The staged key’s catalog, re-read through the staged key: the model step after a reload. |
| GET | /api/v1/credentials/pending/checks | admin | The staged key’s latest check: what the wizard resumes from. |
| POST | /api/v1/credentials/pending/checks | admin | Wizard step 4: run the staged key’s checks with the chosen model. |
| POST | /api/v1/credentials/pending/promote | admin | Wizard step 5: confirm a replacement, and the staged key becomes the connection. |
| DELETE | /api/v1/credentials/{credential_id} | admin | Delete Credential |
| POST | /api/v1/credentials/{credential_id}/model/checks | admin | Change model: test a model on the live key, and store it when it passes. |
| POST | /api/v1/credentials/{credential_id}/verify | admin | Verify: the tenant, the stored model and every paired agent, on the live key. |
GET /api/v1/credentials
Section titled “GET /api/v1/credentials”The live connection and the key staged beside it; either may be null.
Which wizard step the console opens on comes from here and the staged key’s latest check (GET /credentials/pending/checks), so a reload resumes where the operator was. Masked suffixes only: there is no endpoint that returns a key.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CredentialConnection |
GET /api/v1/credentials/checks/{check_id}
Section titled “GET /api/v1/credentials/checks/{check_id}”One check of either key, as far as it has got. Every 202 here names this.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
check_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CredentialCheckView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/credentials/pending
Section titled “POST /api/v1/credentials/pending”Wizard step 2: connect with a new key and stage it, or refuse it with Kindo’s reason.
Synchronous - about a second. The key is probed (which auth header it takes) and its catalog read before anything is written. On any Kindo failure this answers 422 kindo_connect_failed with Kindo’s reason and stores nothing, so the operator is back on step 1 with the key still in the field. On success the key is staged, replacing any key staged before it and never the live one, and the answer carries the catalog for the model step with a model preselected: the live model if this key offers it, else SIFT_COPILOT_MODEL if offered, else the first suggested.
Requires: admin
Body: StagePendingRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | StagedCredential |
422 | Validation Error | HTTPValidationError |
DELETE /api/v1/credentials/pending
Section titled “DELETE /api/v1/credentials/pending”Cancel: delete the staged key and its checks. The live connection is untouched.
Answers 200 whether or not a key was staged - Cancel after the key expired has nothing left to do, and that is not an error.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Ack |
GET /api/v1/credentials/pending/catalog
Section titled “GET /api/v1/credentials/pending/catalog”The staged key’s catalog, re-read through the staged key: the model step after a reload.
failed_models names every model that failed its test on this staged key, with the reason; a check of any of them is refused.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ModelCatalog |
GET /api/v1/credentials/pending/checks
Section titled “GET /api/v1/credentials/pending/checks”The staged key’s latest check: what the wizard resumes from.
404 when nothing is staged or the staged key has not been checked - the wizard then opens on the model step.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CredentialCheckView |
POST /api/v1/credentials/pending/checks
Section titled “POST /api/v1/credentials/pending/checks”Wizard step 4: run the staged key’s checks with the chosen model.
kind is replace when a live connection exists, else connect. Refused with 409 while a check of this key is running (naming it), or when the key already failed its tenant check - go back to step 1; with 422 for a model the staged key’s catalog does not list, or one that failed its test on this key.
A connect whose checks pass is promoted by the worker. A replace waits for POST /credentials/pending/promote.
Requires: admin
Body: ModelCheckRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/credentials/pending/promote
Section titled “POST /api/v1/credentials/pending/promote”Wizard step 5: confirm a replacement, and the staged key becomes the connection.
409 unless the staged key’s latest check passed (warnings allowed) and finished under an hour ago. One transaction: the old key and its checks go, the staged key is live, and the audit names both.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CredentialSummary |
DELETE /api/v1/credentials/{credential_id}
Section titled “DELETE /api/v1/credentials/{credential_id}”Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
credential_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Ack |
422 | Validation Error | HTTPValidationError |
POST /api/v1/credentials/{credential_id}/model/checks
Section titled “POST /api/v1/credentials/{credential_id}/model/checks”Change model: test a model on the live key, and store it when it passes.
The model test only; no staging and no confirmation. 422 for a model the live catalog does not list, or one that failed its test on this key in the last hour; 409 while a check of this key is running.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
credential_id | path | yes | string |
Body: ModelCheckRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/credentials/{credential_id}/verify
Section titled “POST /api/v1/credentials/{credential_id}/verify”Verify: the tenant, the stored model and every paired agent, on the live key.
A reverify check on the worker. The tenant task records which auth header worked, verified_at and any error on the row, as Verify always did; a stored model the catalog no longer lists fails with kindo_model_unavailable until another is chosen. 409 while a check of this key is running.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
credential_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/kindo/agents | admin, operator, reviewer | One page of the tenant’s agents, with the upstream cursor passed straight through. |
| GET | /api/v1/kindo/agents/{agent_id} | admin, operator, reviewer | One agent, by id. |
| GET | /api/v1/kindo/integrations | admin, operator, reviewer | The tenant’s connected integrations (integration_rows), each with when Sift last read it (last_read_at, from the read hints). |
| GET | /api/v1/kindo/integrations/{slug}/check | admin, operator | Whether Sift can read this integration, before any conversation starts. |
| GET | /api/v1/kindo/models | admin, operator, reviewer | The live key’s model catalog, grouped as every model selector shows it. |
GET /api/v1/kindo/agents
Section titled “GET /api/v1/kindo/agents”One page of the tenant’s agents, with the upstream cursor passed straight through.
The page is fetched by list_agents_page, which builds its query parameters through :class:~sift.kindo.pagination.PageRequest. That is the point: the request for a cursor page is assembled from the same :class:~sift.kindo.pagination.PageWindow as the first one, so any filter on the walk survives the follow-up. A hand-rolled loop that forwarded only the cursor would still return 200s full of plausible rows while quietly describing a different population (tech spec section 4.2).
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_KindoAgentSummary_ |
422 | Validation Error | HTTPValidationError |
GET /api/v1/kindo/agents/{agent_id}
Section titled “GET /api/v1/kindo/agents/{agent_id}”One agent, by id.
The not-found case is a first-class answer here, because this endpoint is what the console calls to check an id somebody typed. It is the cheap way to find out that an id names nothing, before an operator pays for an analysis of it.
So the two ways Kindo says no are translated. It answers 404 for a well-formed id that is not in the tenant, and 400 for one that is not a UUID at all - and relaying either verbatim put its request line in front of an operator: “GET /v1/agents/agt_typo returned HTTP 400. Invalid request parameters: ‘agentId - invalid uuid’”. Every word of that is true and none of it is addressed to the person who mistyped an id into a box.
Anything else - an unreadable credential, a platform outage - is left alone. Those are not this id’s fault and the operator’s remedy is different, which is precisely the distinction a single “not found” would destroy.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
agent_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | KindoAgentDetail |
422 | Validation Error | HTTPValidationError |
GET /api/v1/kindo/integrations
Section titled “GET /api/v1/kindo/integrations”The tenant’s connected integrations (integration_rows), each with when Sift last read it (last_read_at, from the read hints).
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of KindoIntegration |
GET /api/v1/kindo/integrations/{slug}/check
Section titled “GET /api/v1/kindo/integrations/{slug}/check”Whether Sift can read this integration, before any conversation starts.
for is what the operator described or picked; it ranks the heuristic’s tools. Three tiers, stopping at the first that reads - this tenant’s read hint, the read the starters model proposed (from the starters cache only: no model call is made here), and a generic heuristic over the integration’s tools - in at most three tool calls of ten seconds each. See :mod:sift.services.source_check.
Tool failures are answers, not errors: unreadable with the reason and its kind. unknown means no tier had anything to try. Cached per slug and for for ten minutes, and the latest answer per slug is what a new copilot session seeds from. A failure to reach Kindo at all (no credential) is still an error, as on every route here.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
slug | path | yes | string | |
for | query | no | string | Default “. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SourceCheck |
422 | Validation Error | HTTPValidationError |
GET /api/v1/kindo/models
Section titled “GET /api/v1/kindo/models”The live key’s model catalog, grouped as every model selector shows it.
The same shape as the connection wizard’s model step: suggested families first, then the rest as listed, with the model Sift’s own calls use now (selected) and the one a selector should open on (preselected: Sift’s model if this key offers it). Open to every role, because the agent screens offer a model too. failed_models lists models that failed their test on this key in the last hour; Change model refuses them.
agent_models is what an agent can be generated on: the models seen running in this Kindo, the default first, under the id Kindo honours in an agent. The catalog’s ids name models for Sift’s own calls and not for an agent, which Kindo would run on its default instead.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ModelCatalog |
analysis
Section titled “analysis”Analysis spends real money, both in LLM calls metered against your own governance and in the volume of API calls a portfolio scan makes. The portfolio scan is API-only: the console neither starts one nor shows its ranking.
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/analysis | admin, operator | List Reports |
| POST | /api/v1/analysis/agents | admin, operator | Queue an analysis of one agent’s run history. |
| POST | /api/v1/analysis/portfolio-scan | admin, operator | Queue a ranking of every agent by filtering opportunity. |
| GET | /api/v1/analysis/portfolio-scan/{scan_id} | admin, operator | One portfolio scan and its ranking, as far as it has got. |
| GET | /api/v1/analysis/{report_id} | admin, operator | One report. |
GET /api/v1/analysis
Section titled “GET /api/v1/analysis”Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
agent_id | query | no | string, nullable | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_AnalysisReport_ |
422 | Validation Error | HTTPValidationError |
POST /api/v1/analysis/agents
Section titled “POST /api/v1/analysis/agents”Queue an analysis of one agent’s run history.
Reads what the agent did across hundreds of real runs rather than what it was configured to do, because prompts and step configuration are not exposed by the API and observed behaviour is the better input regardless. That is minutes of work, so this returns as soon as the report row exists and the job is on the queue.
Requires: admin, operator
Body: AnalysisRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/analysis/portfolio-scan
Section titled “POST /api/v1/analysis/portfolio-scan”Queue a ranking of every agent by filtering opportunity.
API-only: the console does not offer a portfolio scan or show its results. Read them with GET /analysis/portfolio-scan/{scan_id}.
Two-pass and cost-bounded: a shallow sampled pass over all agents, then a deep pass only on candidates clearing a first-pass futility threshold. A deep scan of 500 agents with full history is a large number of API calls and a real LLM bill, which is exactly why it does not run inside an HTTP request.
Requires: admin, operator
Body: PortfolioScanRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
GET /api/v1/analysis/portfolio-scan/{scan_id}
Section titled “GET /api/v1/analysis/portfolio-scan/{scan_id}”One portfolio scan and its ranking, as far as it has got.
API-only, like the scan itself: nothing in the console reads this.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
scan_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PortfolioScan |
422 | Validation Error | HTTPValidationError |
GET /api/v1/analysis/{report_id}
Section titled “GET /api/v1/analysis/{report_id}”One report.
Carries its exclusion rate and any conditioning note alongside the headline, not in a footnote: both determine whether the headline can be believed. A report that failed carries its reason in error and no recommendation, so it cannot be read as a recommendation of no_action.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
report_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | AnalysisReport |
422 | Validation Error | HTTPValidationError |
copilot
Section titled “copilot”The copilot proposes configuration and cannot apply it. It holds no write tools by design.
| Method | Path | Requires | Summary |
|---|---|---|---|
| POST | /api/v1/copilot/intake | admin, operator | Read what the operator typed, and either refuse it or say where it lands. |
| GET | /api/v1/copilot/sessions | admin, operator | The caller’s unexpired conversations, newest change first. |
| POST | /api/v1/copilot/sessions | admin, operator | Open a conversation. |
| GET | /api/v1/copilot/sessions/{session_id} | admin, operator | Get Session |
| POST | /api/v1/copilot/sessions/{session_id}/analysis | admin, operator | Hand a finished analysis to a conversation already under way. |
| POST | /api/v1/copilot/sessions/{session_id}/deliver-replay | admin, operator | What Shape delivery would save on the session’s last sample. |
| POST | /api/v1/copilot/sessions/{session_id}/messages | admin, operator | Send a message and stream the reply as server-sent events. |
| GET | /api/v1/copilot/sessions/{session_id}/sweep | admin, operator | The session’s last sample as the review step reads it, and its latest sweep. |
| POST | /api/v1/copilot/sessions/{session_id}/sweep | admin, operator | Sweep Collapse’s similarity settings over the session’s last sample. |
| GET | /api/v1/copilot/starters/{slug} | admin, operator | Ways to start a filter over one integration, before anyone has typed anything. |
POST /api/v1/copilot/intake
Section titled “POST /api/v1/copilot/intake”Read what the operator typed, and either refuse it or say where it lands.
The door for somebody who has a sentence and no source, and the guard on the one who has a source and adds a sentence to it. Same call either way: with integration_slugs the question is “can these sources do this”, without them it is “which of my sources could”.
It creates the session. The flow needs somewhere durable to keep the off-topic count from the first natural-language turn, which on this path is before anything else exists, and a count the console kept would be a second answer to a question that has one - and would reset on reload, which is the one moment it matters.
A refusal is a 200 with refusal set, not an error status. The request was well-formed and Sift answered it; what it answered is that it will not act. A 4xx would put “you sent something wrong” on a screen where the operator sent exactly what they were asked for.
Requires: admin, operator
Body: IntakeRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | IntakeResult |
422 | Validation Error | HTTPValidationError |
GET /api/v1/copilot/sessions
Section titled “GET /api/v1/copilot/sessions”The caller’s unexpired conversations, newest change first.
At most 20. Declined and abandoned sessions are listed and flagged, and so is one that has already become a filter; resumable says which are drafts worth going back to, and the console lists only those. A session is listed once it holds a message, and sessions opened before Sift recorded who opened them are in nobody’s list. Read from a per-user index, never a scan of every session. 422 for mine=false: there is no listing of other people’s sessions.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
mine | query | no | boolean | Must be true: a caller lists their own sessions, and only those. Default True. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CopilotSessionList |
422 | Validation Error | HTTPValidationError |
POST /api/v1/copilot/sessions
Section titled “POST /api/v1/copilot/sessions”Open a conversation.
With filter_id, it writes that filter’s first spec: the proposal carries the filter’s id and name, and the console saves it as version 1 onto the filter rather than creating another. 404 for a filter that does not exist; 409 unless it is draft or authoring and has no spec.
Requires: admin, operator
Body: StartSessionRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | CopilotSession |
422 | Validation Error | HTTPValidationError |
GET /api/v1/copilot/sessions/{session_id}
Section titled “GET /api/v1/copilot/sessions/{session_id}”Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CopilotSession |
422 | Validation Error | HTTPValidationError |
POST /api/v1/copilot/sessions/{session_id}/analysis
Section titled “POST /api/v1/copilot/sessions/{session_id}/analysis”Hand a finished analysis to a conversation already under way.
The console collects the description first and only then runs the analysis, so the session exists before the evidence does. Attaching is a real event rather than a creation-time-only option: the operator says what they want, Sift measures the agent they are modelling, and the findings arrive partway through.
Refuses to replace one report with another. A transcript already reasoning from one set of findings and then handed a different set would be arguing from evidence the operator never saw it reason about.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
Body: AttachAnalysisRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CopilotSession |
422 | Validation Error | HTTPValidationError |
POST /api/v1/copilot/sessions/{session_id}/deliver-replay
Section titled “POST /api/v1/copilot/sessions/{session_id}/deliver-replay”What Shape delivery would save on the session’s last sample.
The review step’s draft: a new filter escalates everything, so the sample is replayed as escalations through the draft stage’s settings and every preset, the same replay a running stage’s runs_saved evidence reads. Pure Python, answered at once; nothing is written, and the answer is aggregate figures only.
404 for a session that has expired or never existed; 409 when the session has no sample or no proposal yet; 422 when params are not Shape delivery’s.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
Body: SampleDeliverReplayRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SampleDeliverReplay |
422 | Validation Error | HTTPValidationError |
POST /api/v1/copilot/sessions/{session_id}/messages
Section titled “POST /api/v1/copilot/sessions/{session_id}/messages”Send a message and stream the reply as server-sent events.
Streamed because a turn samples a live source and profiles it, which takes long enough that a spinner is not an acceptable answer.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
Body: SendMessageRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | any |
422 | Validation Error | HTTPValidationError |
GET /api/v1/copilot/sessions/{session_id}/sweep
Section titled “GET /api/v1/copilot/sessions/{session_id}/sweep”The session’s last sample as the review step reads it, and its latest sweep.
The copilot’s duplicate rate and the sample’s timestamp coverage are read from the sample on every call. sweep is null until one is requested, then pending until the ML worker answers. 404 once the session has expired.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SampleSweepView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/copilot/sessions/{session_id}/sweep
Section titled “POST /api/v1/copilot/sessions/{session_id}/sweep”Sweep Collapse’s similarity settings over the session’s last sample.
Runs dedup_sweep on the ML worker, where the encoder is staged, and answers 202 at once; poll GET .../sweep. The sample never leaves the session: the worker reads it from there, and what it writes back is aggregate figures that expire with the session. When under 90% of the sample carries a usable timestamp, the sweep runs in one pass and its figures are upper bounds (sliced: false). A new request replaces the last.
404 for a session that has expired or never existed; 409 when the session has no sample yet; 422 when there is no text field to compare, or params are not Collapse’s.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
session_id | path | yes | string |
Body: SampleSweepRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
GET /api/v1/copilot/starters/{slug}
Section titled “GET /api/v1/copilot/starters/{slug}”Ways to start a filter over one integration, before anyone has typed anything.
This is the common path and it must not require writing. An operator who knows they want to cut PagerDuty noise usually cannot yet write the sentence that says which incidents matter, and a blank textarea asks them to. Three starting points they can click, edit, or ignore is a far smaller ask, and every one of them is still only a description - the filter itself is proposed later, from sampled data, the same as it always was.
Generated rather than curated. A curated table would cover the vendors somebody thought of and leave this tenant’s other forty connections with nothing, and “nothing” for an integration somebody just clicked is the worst answer on the screen.
Cached for a day, keyed on the slug. Generation is a model call of a couple of seconds, and it is the same call for everyone looking at the same integration: two operators opening PagerDuty an hour apart should not be comparing different lists, let alone paying twice for the privilege.
It also carries read: the call the same model reply proposes to start reading from, held to the tool’s inputSchema before it is kept. The source check replays it from this cache as its second tier, and never makes a model call of its own.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
slug | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | StarterSet |
422 | Validation Error | HTTPValidationError |
filters
Section titled “filters”A filter is created in authoring when the request carries a spec, and in draft without one. It reaches active only through a gold-set approval and a threshold approval, both recorded.
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters | admin, operator, reviewer | A page of filters, newest first, optionally narrowed to some states. |
| POST | /api/v1/filters | admin, operator | Create a filter: in authoring with a spec, in draft without one. |
| POST | /api/v1/filters/load-preview | admin, operator, reviewer | The volume preflight for a draft, before a filter exists. |
| GET | /api/v1/filters/{filter_id} | admin, operator, reviewer | Get Filter |
| POST | /api/v1/filters/{filter_id}/asks/{code}/decline | admin, operator | Decline a recommendation: not on this evidence, with a reason. |
| GET | /api/v1/filters/{filter_id}/bakeoff | admin, operator, reviewer | What each recipe on the slate has shown, and which one has the best argument. |
| PUT | /api/v1/filters/{filter_id}/candidates | admin, operator | Replace Decide’s slate: PUT .../stages/decide/candidates, kept for one release. |
| POST | /api/v1/filters/{filter_id}/cutover/acknowledge | admin, operator | Acknowledge that the agent this filter’s pairing displaced may keep running. |
| GET | /api/v1/filters/{filter_id}/decision-fit | admin, operator, reviewer | How well the decision model reads this filter: the band in force, the newest reading, and the last ten, oldest first. |
| POST | /api/v1/filters/{filter_id}/decision-fit/measure | admin, operator | Take a fit reading now, on the io worker (Measure now). |
| GET | /api/v1/filters/{filter_id}/fields | admin, operator, reviewer | The field paths the filter’s recent events carry, for a field picker. |
| GET | /api/v1/filters/{filter_id}/load-preview | admin, operator, reviewer | The volume preflight for this filter’s current spec. |
| POST | /api/v1/filters/{filter_id}/pair | admin, operator | Confirm that agent_id is the agent this filter feeds. |
| POST | /api/v1/filters/{filter_id}/pair/preview | admin, operator | What pairing agent_id would do, without pairing it. |
| POST | /api/v1/filters/{filter_id}/paired-agent | admin, operator | The agent built to consume this filter’s output, ready to import. |
| POST | /api/v1/filters/{filter_id}/paired-agent/create | admin, operator | Create the generated agent in Kindo and pair this filter to it. |
| POST | /api/v1/filters/{filter_id}/paired-agent/create/preview | admin, operator | What Create in Kindo and connect would do to the filter, before anything exists. |
| POST | /api/v1/filters/{filter_id}/pause | admin, operator | Stop reading the source: nothing is filtered, and no agent run is started. |
| POST | /api/v1/filters/{filter_id}/restore-point | admin, operator | Return a filter at its safer point to its approved point. |
| POST | /api/v1/filters/{filter_id}/resume | admin, operator | Filter again on the model that was running, re-affirming its approval. |
| POST | /api/v1/filters/{filter_id}/stages/guarantee/try | admin, operator | Try it: ask one always-escalate rule of up to 200 recent filtered events. |
| PUT | /api/v1/filters/{filter_id}/stages/{slot} | admin, operator | Set the stage in a slot (id, params, mode), or remove it with null. |
| PUT | /api/v1/filters/{filter_id}/stages/{slot}/candidates | admin, operator | Replace the slate of a slot whose stage carries candidates. Touches no stage. |
| GET | /api/v1/filters/{filter_id}/stages/{slot}/evidence | admin, operator, reviewer | Every evidence block the stage in a slot reads, in full, each with its kind. |
| GET | /api/v1/filters/{filter_id}/stages/{slot}/examples | admin, operator, reviewer | Examples of what the stage in a slot did over its window, largest group first. |
| POST | /api/v1/filters/{filter_id}/stages/{slot}/measure | admin, operator | Run the stage’s evidence now, on the ML worker. |
| POST | /api/v1/filters/{filter_id}/stages/{slot}/preview | admin, operator, reviewer | What PUT .../stages/{slot} with this stage would do, without saving it. |
| POST | /api/v1/filters/{filter_id}/stages/{slot}/winner | admin, operator | Promote a candidate within its slot: it becomes that stage’s recipe, active. |
| POST | /api/v1/filters/{filter_id}/state | admin, operator | Move a filter through the lifecycle. |
| GET | /api/v1/filters/{filter_id}/verdict-briefing | admin, operator, reviewer | The verdict contract, as text to paste into an agent Sift did not write. |
| GET | /api/v1/filters/{filter_id}/versions | admin, operator, reviewer | Every version, newest first: the one running marked current, and a version waiting behind it for approval, or one that waited and was discarded, marked pending. Neither of those two ever ran. |
| POST | /api/v1/filters/{filter_id}/versions | admin, operator | Record a new spec version. |
| POST | /api/v1/filters/{filter_id}/versions/pending/measure | admin, operator | Queue the waiting version’s re-measurement again. |
| POST | /api/v1/filters/{filter_id}/versions/preview | admin, operator, reviewer | What saving this spec would do, without saving it. |
| GET | /api/v1/filters/{filter_id}/warm-start | admin, operator, reviewer | Whether a warm start is offered for this filter now, and the latest one. |
| POST | /api/v1/filters/{filter_id}/warm-start | admin, operator | Have the paired agent judge events events of this filter’s history. |
| POST | /api/v1/filters/{filter_id}/winner | admin, operator | Make a candidate Decide’s recipe: POST .../stages/decide/winner, for one release. |
GET /api/v1/filters
Section titled “GET /api/v1/filters”A page of filters, newest first, optionally narrowed to some states.
state repeats: ?state=shadow&state=proposed&state=active. The console groups the eight lifecycle states into three a person actually chooses between, and a group has to be narrowed here rather than after the fact - a page filtered in the browser is a page with the wrong number of rows on it.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
state | query | no | array of FilterState, nullable | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_FilterSummary_ |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters
Section titled “POST /api/v1/filters”Create a filter: in authoring with a spec, in draft without one.
A new filter filters nothing. It reaches active only through a gold-set approval and a threshold approval, both recorded. With a spec, Sift takes draft -> authoring itself, attributed to system.
copilot_session_id names the conversation it came from, and is recorded on that session when it is still there and is the caller’s; a session that is gone never fails the create.
Requires: admin, operator
Body: CreateFilterRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/load-preview
Section titled “POST /api/v1/filters/load-preview”The volume preflight for a draft, before a filter exists.
The review step’s spec as edited: how big its source is, as a range with the basis it was read on, what shadow would cost the agent, and the protections that fit. Reads only; nothing is switched on. A draft has no polls, so the basis is what the operator said (stated), a read of the spec’s own call (sampled), the run history of analysis_report_id or of the spec’s agent (agent_history), or unknown with the reason.
Requires: admin, operator, reviewer
Body: DraftLoadPreviewRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | LoadPreview |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}
Section titled “GET /api/v1/filters/{filter_id}”Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/asks/{code}/decline
Section titled “POST /api/v1/filters/{filter_id}/asks/{code}/decline”Decline a recommendation: not on this evidence, with a reason.
The ask leaves asks for the recipe it names until the holdout that recipe is measured on has grown by a tenth; a different recipe recommended is shown. Recorded, with the holdout it was declined on, and audited as filter.recommendation_declined. Nothing about the filter changes: a recommendation is advice, and declining it filters and escalates exactly what taking no notice of it would.
422 for a code that is not a recommendation’s: a condition is cleared by being fixed, never dismissed, as on the Alerts page. 409 unless slot and recipe_id name the recommendation the filter’s asks carry now. 404 for an unknown filter.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
code | path | yes | string |
Body: DeclineRecommendationRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/bakeoff
Section titled “GET /api/v1/filters/{filter_id}/bakeoff”What each recipe on the slate has shown, and which one has the best argument.
The slot whose stage carries candidates only (Decide): that stage and its candidates, with slot on the view and on every row. A stage in another slot never competes with it; its evidence is GET .../stages/{slot}/evidence. 409 when the filter has no spec, or no stage in that slot (a filter that only collapses repeats).
Read-only, and the recommendation is advisory. A screen that promoted on its own reading of a holdout would route around the one gate this system is built on.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | BakeoffView |
422 | Validation Error | HTTPValidationError |
PUT /api/v1/filters/{filter_id}/candidates
Section titled “PUT /api/v1/filters/{filter_id}/candidates”Replace Decide’s slate: PUT .../stages/decide/candidates, kept for one release.
See that endpoint. A recipe of another slot is refused here as there: it is a stage of its own (PUT .../stages/{slot}), never a candidate.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: UpdateCandidatesRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | BakeoffView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/cutover/acknowledge
Section titled “POST /api/v1/filters/{filter_id}/cutover/acknowledge”Acknowledge that the agent this filter’s pairing displaced may keep running.
Not a claim that it was retired: the person accepts that if it keeps running on this traffic, it and the filter act on the same events and its runs are paid for too. That settles the cutover reading as acknowledged everywhere it is read - the filter’s asks, the Alerts page, /system/status, and the cost panel, which then reports spend saved and names who acknowledged it - and Sift stops checking the agent. Only a new pairing asks again. Audited as filter.cutover_acknowledged, with the agent and the note.
409 unless the filter displaced an agent, superseded_agent_id is that agent, and it is not acknowledged already. 404 for an unknown filter.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: AcknowledgeCutoverRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/decision-fit
Section titled “GET /api/v1/filters/{filter_id}/decision-fit”How well the decision model reads this filter: the band in force, the newest reading, and the last ten, oldest first.
The band is the newest measured reading’s, however old; when no new reading can be taken now (no model configured, or it is not answering) it is served as of when it was taken, summary.stale, with the reason. never_measured with no reading. Read from the database and settings only: no model is asked.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | DecisionFitView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/decision-fit/measure
Section titled “POST /api/v1/filters/{filter_id}/decision-fit/measure”Take a fit reading now, on the io worker (Measure now).
About 300 questions to the decision model, metered as fit. It changes nothing the filter does. 409 with the reason when no model can be asked, for a mock filter, or for a filter not yet scoring. A reading already queued is not queued twice: the answer is still 202, naming it.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/fields
Section titled “GET /api/v1/filters/{filter_id}/fields”The field paths the filter’s recent events carry, for a field picker.
Read from stored payloads, never the source: no Kindo call, and an answer while the source is down. Empty with a reason rather than an error where there is nothing to read. Every path is customer data and is rendered through Untrusted.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | EventFieldsView |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/load-preview
Section titled “GET /api/v1/filters/{filter_id}/load-preview”The volume preflight for this filter’s current spec.
How big the stream is, as a range with its basis; what shadow sends the agent under the spec as it stands (full shadow, a sampled share, or capped by a budget) and what that costs a day when a run is priced; the agent’s capacity and the deployment’s Kindo bucket; the protections that fit; and what the ceiling’s self-defeat line needs. Start in shadow’s confirm and Created read their numbers here. Reads only.
409 for a draft with no spec yet; 422 when stated_events_per_hour comes without stated=custom or the other way round.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
stated | query | no | under_1k | 1k_10k | 10k_100k | over_100k | custom, nullable | What the operator said the stream is: a preset, or custom. |
stated_events_per_hour | query | no | number, nullable | With stated=custom, and only with it. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | LoadPreview |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/pair
Section titled “POST /api/v1/filters/{filter_id}/pair”Confirm that agent_id is the agent this filter feeds.
The way in for an agent Sift did not create: one imported by hand from the download, where the key or the Kindo cannot create, or one somebody else built. Between the download and this call the trail is a person and a browser. Reading the agent back and checking it declares the contract input is the only evidence available that the thing which was imported is the thing that was generated. /paired-agent/create pairs through the same checks.
Pairing also converts the filter to blob delivery, as a new spec version rather than an edit: what the filter sends and which agent it starts are both changing, and a change of that size belongs in the version history where an approver can see it. A changed agent is a gated change, so on an active filter that version returns it to shadow until a threshold is approved for it. With keep_filtering and a rationale, and only when one blob-delivery agent replaces another while an approval stands, the version is saved as a re-affirmation of that approval instead and the filter keeps filtering; /pair/preview says whether that would be accepted, and the pairing refuses it otherwise.
It is also the last moment the displaced agent is knowable. spec here still names the agent the filter was modelled on; the paired spec below does not, and that agent keeps running until somebody retires it. Recording it, with a baseline to compare against later, is Phase 6’s whole handle on the cutover.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: PairAgentRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/pair/preview
Section titled “POST /api/v1/filters/{filter_id}/pair/preview”What pairing agent_id would do, without pairing it.
The agent is read back and checked as the pairing checks it, and the spec change the pairing would write is classified as a spec save’s preview classifies one. So the console can say, before the button is pressed, whether the filter would return to shadow, and whether keep_filtering would be accepted instead. Advisory: the pairing checks again when it lands.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: PairAgentRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PairPreview |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/paired-agent
Section titled “POST /api/v1/filters/{filter_id}/paired-agent”The agent built to consume this filter’s output, ready to import.
Generated from a paired view of the stored spec rather than from the spec itself. A filter that still maps event fields onto someone else’s agent is not converted here: adopting the generated agent changes where this filter sends events and which agent it starts, which is a decision an operator makes by importing and pairing, not a side effect of asking to see the file.
Deterministic, so asking twice produces the same bytes and a regenerated definition diffs cleanly against one already imported - for the same model. The steps leave the agent on Kindo’s default model unless model_id picks one Sift has seen run (_step_model), and nothing about the choice is stored, so a later download starts on the default again.
For a filter with a paired agent it also says what that agent runs on, what Sift asked for, and when the two differ (_paired_agent_model).
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: PairedAgentRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PairedAgentDefinition |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/paired-agent/create
Section titled “POST /api/v1/filters/{filter_id}/paired-agent/create”Create the generated agent in Kindo and pair this filter to it.
The download, the import and the pasted-back id, as one action. Creating is pairing: the agent is created only to be paired to, through the same read-back, refusal, version and cutover /pair runs.
In order, because Kindo can create and cannot delete:
- Everything
/pair/previewwould refuse is refused first - no spec, a re-affirmation with no reason or nothing to re-affirm, a version the pairing could not write - so a refusal leaves nothing behind in Kindo. - The definition is the one the download serves, with a fresh
sift-create:marker as the last line of its description. - It is created, once. When the answer is lost the marker is looked for, in one scan of the agent list: found, Sift carries on with that agent; not found, the answer is a 503 saying nothing was paired. Nothing is retried; pressing again is the operator’s call, made knowing what Sift could and could not tell.
- The agent is read back once, and
filter.agent_createdis committed with what was read - its owner and its models - before pairing is attempted. A pairing refused after this keeps the record, and the error names the agent indetails.created_agent_idso the console can offer to pair to it. - The pairing
/pairruns, on that read-back. Then it commits.
A key Kindo forbids to create agents is a 409 with Kindo’s reason; so is a Kindo with no create endpoint, which is also remembered for an hour so /paired-agent can say create_available: false. The download path works throughout.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: CreatePairedAgentRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | CreatedPairing |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/paired-agent/create/preview
Section titled “POST /api/v1/filters/{filter_id}/paired-agent/create/preview”What Create in Kindo and connect would do to the filter, before anything exists.
/pair/preview for the agent a create has not made yet. There is no agent to read back, so Kindo is not asked: the version pairing would write is judged against the stand-in id the create judges it against (agent_id in the answer, and in changes), and refusal is always None. The create reads the real agent back after making it, and refuses then if it must.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PairPreview |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/pause
Section titled “POST /api/v1/filters/{filter_id}/pause”Stop reading the source: nothing is filtered, and no agent run is started.
A pause, which the console calls Hold events. Sift stops polling the filter, so it scores nothing and triggers nothing, and its agent receives nothing from Sift until the filter scores again. To stop filtering while every event keeps reaching the agent, move an active filter to shadow with POST /filters/{id}/state instead.
Ungated, like that move, and it takes no body: nothing should stand between an operator and either.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/restore-point
Section titled “POST /api/v1/filters/{filter_id}/restore-point”Return a filter at its safer point to its approved point.
For a filter on on_alarm: loosen that Sift moved to its safer point on a drift breach. Writes a version at the threshold the standing approval names, re-affirmed with the rationale, and acknowledges the signals breached now for up to seven days or until they clear, as a resume does; any other breach moves it again. The filter stays active. 409 not_at_safer_point for a filter that is not there.
An operator’s or an administrator’s act, as a resume is.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: RestorePointRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/resume
Section titled “POST /api/v1/filters/{filter_id}/resume”Filter again on the model that was running, re-affirming its approval.
For a filter Sift took out of service on a drift alarm or its ceiling, whose withdrawn approval a person now re-affirms with a written reason; and for one a person moved out of service with its approval standing. The signals breached now are acknowledged for up to seven days or until they clear, and any other breach falls back again. Refused, with the reason, for a version never approved, after a gated change, and after an unapproved fall-back: the threshold screen offers the running model for those.
An operator’s or an administrator’s act, as approving a threshold is.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: ResumeRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/stages/guarantee/try
Section titled “POST /api/v1/filters/{filter_id}/stages/guarantee/try”Try it: ask one always-escalate rule of up to 200 recent filtered events.
Nothing is saved and nothing is sent: the answer is what the rule would add, as extra escalations a day, with up to five events it fires on. About 200 questions to the decision model, metered as rescue. 409 with the reason when no model can be asked, or for a mock filter, which never asks one.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: GuaranteeTryRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | GuaranteeTryResult |
422 | Validation Error | HTTPValidationError |
PUT /api/v1/filters/{filter_id}/stages/{slot}
Section titled “PUT /api/v1/filters/{filter_id}/stages/{slot}”Set the stage in a slot (id, params, mode), or remove it with null.
Writes a version through create_version, classified by the slot’s own change rule: removing a Collapse stage or setting it to shadow is free, and carries the standing approval; switching one on, loosening it, or changing Decide while it decides is gated. On an active filter a gated change that leaves the deciding stage as it is waits (below); one that changes the deciding stage takes the filter to shadow until the new version’s point is approved - except a retune (similarity, window, cluster-size cap) of a Collapse stage the filter filters through alone, which waits too, measured on the stage’s own sweep; changing the fields it compares still falls back. Every other stage stays as it is: Decide joining a filter that only collapses repeats is an addition, never a swap. The stage keeps its candidates. Removing the slot that reads the threshold also resets the threshold to 0.0, which nothing then reads.
Answers with the version, what changed per stage, the filter’s state after, and pending: on an active filter, a gated change to stages alone that leaves the deciding stage as it is waits behind the running version. The filter stays active and keeps filtering on the version it runs, pending_sentence says so, and its measurement is taken: the running model re-scored through the new pipeline (queued), or for a Collapse-only filter’s retune, read at save time off the stage’s own compression sweep; GET /threshold offers it for approval. With a version already waiting, the write is made on top of it and replaces it (replaced_pending_version).
404 for a slot the registry has no entry for; 409 for one not built in this build, a filter with no spec, or a write that changes nothing, and recipe_unavailable for a recipe this install cannot run or an administrator turned off; 422 for a stage the spec refuses (a recipe of another slot, params that are not its recipe’s, a pipeline left with no stage that can filter) or for removing a stage the slot does not have.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
Body: SetStageRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | StageWriteResult |
422 | Validation Error | HTTPValidationError |
PUT /api/v1/filters/{filter_id}/stages/{slot}/candidates
Section titled “PUT /api/v1/filters/{filter_id}/stages/{slot}/candidates”Replace the slate of a slot whose stage carries candidates. Touches no stage.
This writes a new spec version, and approvals are scoped to the version they approved. create_version classifies the change: the slate is free, because a candidate never reaches the decision path, so the standing threshold approval is carried to the new version. differs_in_filtering below is the second, independent statement of the same property.
That distinction is the whole reason the bake-off is usable. Without it, turning a candidate on would silently stop a filter filtering, and an operator who lost their approval by looking at a comparison screen would learn to leave the slate alone.
The slate arrives whole rather than as add and remove operations, because the console edits it as one thing and two partial edits would interleave into a slate neither client asked for. origin is not accepted from the caller: an operator editing the slate is the operator’s own change, whoever first proposed the entry.
404 for a slot the registry has no entry for; 409 for a slot that compares no candidates, or a filter with no stage there; 422 for a candidate that is not a recipe of the slot.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
Body: UpdateCandidatesRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | BakeoffView |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/stages/{slot}/evidence
Section titled “GET /api/v1/filters/{filter_id}/stages/{slot}/evidence”Every evidence block the stage in a slot reads, in full, each with its kind.
compression_sweep and purity for Collapse, frontier for Decide; FilterDetail.stages carries the headline figures of the same. 404 for an unknown slot or one the filter has no stage in.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | StageEvidenceView |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/stages/{slot}/examples
Section titled “GET /api/v1/filters/{filter_id}/stages/{slot}/examples”Examples of what the stage in a slot did over its window, largest group first.
For Collapse, the window’s largest clusters (Show examples): each cluster’s leader and a few of its repeats, as source event ids and the text the stage compares, cut short. A payload past its retention is not read: that example is its id alone, marked expired. Read on request only, never for the strip.
404 for an unknown slot, one the filter has no stage in, or one that keeps no examples.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | StageExamplesView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/stages/{slot}/measure
Section titled “POST /api/v1/filters/{filter_id}/stages/{slot}/measure”Run the stage’s evidence now, on the ML worker.
For a stage beside the deciding one - a Collapse stage in shadow, most often, on a filter with no Decide model yet - the stage is fitted as training fits it and its evidence recorded (compression_sweep for Collapse), with nothing proposed and no state changed. So is a stage approved on its own evidence: a Collapse stage deciding alone is measured, with no label floor in front of it. For any other deciding stage, this is a training run, as POST .../train.
404 for a slot the registry has no entry for. 409 for a slot not built here, without a spec, without a stage in the slot, for a stage no fit measures (one acting after the decision, whose evidence is read on request), or - for the deciding stage - with the training refusal POST .../train gives (training_refused, e.g. the gold set not approved).
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/stages/{slot}/preview
Section titled “POST /api/v1/filters/{filter_id}/stages/{slot}/preview”What PUT .../stages/{slot} with this stage would do, without saving it.
The spec that write would save, built the same way (the stage’s candidates kept, the threshold reset when the slot that reads it is emptied), answered exactly as POST .../versions/preview answers for it: each changed stage’s rule and sentence, the consequence, whether the approval carries, and pending. Settings > Pipeline shows it per slot before a save.
404 for a slot the registry has no entry for; 409 for one not built here or a filter with no spec; 422 for a stage the spec refuses, or for removing a stage the slot does not have.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
Body: PreviewStageRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SpecChangePreview |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/stages/{slot}/winner
Section titled “POST /api/v1/filters/{filter_id}/stages/{slot}/winner”Promote a candidate within its slot: it becomes that stage’s recipe, active.
Nothing else moves. Every other stage stays as it is: a Collapse stage filtering in front keeps filtering, one measuring in shadow keeps measuring. A recipe of another slot is refused (409): it would not replace this stage’s recipe, it would be a stage of its own.
A filter that is filtering stops. The new spec carries a shadow operating point at threshold 0.0 and the filter moves to shadow, because the approved threshold was measured against the old recipe’s model and says nothing about this one. Carrying it across would leave the filter filtering events at a number nobody measured for the model now running, which is the exact failure the threshold gate exists to prevent. Reducing filtering never needs an approval, which is why this transition can be taken here; earning it back does.
The displaced recipe becomes a disabled candidate rather than being dropped. The spec is where a later reader looks to find that it was the decider and was beaten, and disabling rather than removing keeps that record without paying a scoring pass per event for a recipe that already lost.
404 for a slot the registry has no entry for. 409 when the filter has no spec yet, when the slot compares no candidates, when the recipe stands in another slot, when it is not a candidate on it, when it collapses repeats (it runs in front of a decider and is never one), or when it is not yet eligible to win (the reason is the bake-off’s).
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
slot | path | yes | string |
Body: SelectWinnerRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/state
Section titled “POST /api/v1/filters/{filter_id}/state”Move a filter through the lifecycle.
Legal transitions and the gate each requires are enforced server-side. The UI renders the gates; it does not define them.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: ChangeStateRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/verdict-briefing
Section titled “GET /api/v1/filters/{filter_id}/verdict-briefing”The verdict contract, as text to paste into an agent Sift did not write.
The counterpart to /paired-agent, for the operator who is keeping their own agent. Sift can read a verdict out of conversation metadata, out of a stated field, or - only where it did not write the prompt, and only then - out of prose. The third is a guess, it is the route most often wrong, and on an unbriefed pairing it is the route most verdicts arrive by. This is what closes that gap without asking anybody to adopt a generated agent.
A GET with no body and no filter-specific text, because the briefing genuinely does not vary: it names no filter, no agent and no input. What varies is the note, which says what pasting it would change for this filter.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | VerdictBriefing |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/versions
Section titled “GET /api/v1/filters/{filter_id}/versions”Every version, newest first: the one running marked current, and a version waiting behind it for approval, or one that waited and was discarded, marked pending. Neither of those two ever ran.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_FilterVersionSummary_ |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/versions
Section titled “POST /api/v1/filters/{filter_id}/versions”Record a new spec version.
Approvals are scoped to the version they approved, so a change to the operating point re-opens the threshold gate rather than taking effect on save. reaffirm saves a switch to keep filtering through an alarm with the rationale recorded as a re-affirmation of the standing approval instead; the preview’s reaffirmable says whether it would be accepted.
On an active filter, a gated change to stages alone that leaves the deciding stage as it is waits behind the running version: the answer is pending, the filter keeps filtering, and its measurement is taken (queued, or read off the sweep for a Collapse-only retune). Any save discards a version already waiting, or replaces it if it waits too.
A draft’s first version takes it to authoring, attributed to system, as creating a filter with a spec does; from_analysis_report_id is then recorded as its provenance if it has none. copilot_session_id names the conversation opened for this draft, and is recorded on it as POST /filters does, but only on a session opened for this filter.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: CreateFilterVersionRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | FilterVersionSummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/versions/pending/measure
Section titled “POST /api/v1/filters/{filter_id}/versions/pending/measure”Queue the waiting version’s re-measurement again.
A save queues it once. This is for a measurement that never landed - a broker that refused the job, a worker that died - so a waiting version is never stuck reading measuring. It trains nothing and changes nothing but the measurement. 409 when no version waits.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/versions/preview
Section titled “POST /api/v1/filters/{filter_id}/versions/preview”What saving this spec would do, without saving it.
Which fields differ, which of them need a fresh threshold approval, and the consequence: nothing more, a version that waits for approval, a return to shadow, or a refusal. Advisory - the save classifies again against whatever is current when it lands.
pending says the save would wait behind the running version for its own approval and keep the filter filtering meanwhile, with the line the Pipeline block shows in pending_sentence and on each changed stage; replaces_pending_version names a version already waiting, which the save would replace or leave behind.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: PreviewFilterVersionRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | SpecChangePreview |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/warm-start
Section titled “GET /api/v1/filters/{filter_id}/warm-start”Whether a warm start is offered for this filter now, and the latest one.
Its presets (None, 200 events, 500 events, and Custom’s bounds), what one run costs where runs are priced, the labels training would count now against its floor, and the newest warm start with its progress: chosen, sent, judged, given up on, waiting for the agent budget, and when its training was queued. Reads only.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | WarmStartReading |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/warm-start
Section titled “POST /api/v1/filters/{filter_id}/warm-start”Have the paired agent judge events events of this filter’s history.
Before shadow: the filter is being authored, its gold-set gate is open and its agent paired. Reads a pool of history without moving the watermark, chooses from it (half from the top of the decision model’s ranking where one answers, half evenly), and sends each chosen event to the agent a few at a time, within the agent budget when one is set. Each verdict is a backtest label weighted by how likely its event was to be chosen. When the labels reach the floor a training is queued at once.
409 with the reason when it is not offered, including a second warm start for the same gold set: one runs once per gold set. Audited filter.warm_start.
409 too when the paired agent is not told these events are history (generated before contract 5, or not by Sift) and the request does not carry accept_actions_on_history: true: such an agent acts on them as on live events.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: WarmStartRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | WarmStartView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/winner
Section titled “POST /api/v1/filters/{filter_id}/winner”Make a candidate Decide’s recipe: POST .../stages/decide/winner, for one release.
See that endpoint; the same refusals apply.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: SelectWinnerRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterDetail |
422 | Validation Error | HTTPValidationError |
gold-set
Section titled “gold-set”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters/{filter_id}/gold-set | admin, operator, reviewer | The filter’s latest gold set, or null when it has never had one. |
| POST | /api/v1/filters/{filter_id}/gold-set | admin, operator | Build a gold set from the filter’s own source. |
| POST | /api/v1/filters/{filter_id}/gold-set/approve | admin, operator | Sign off the gold set, unlocking authoring -> shadow. |
| POST | /api/v1/gold-items/{item_id}/label | admin, operator, reviewer | Label Gold Item |
GET /api/v1/filters/{filter_id}/gold-set
Section titled “GET /api/v1/filters/{filter_id}/gold-set”The filter’s latest gold set, or null when it has never had one.
null rather than 404, as GET /filters/{id}/threshold answers. A filter with no gold set is the ordinary state of every new filter, and a 404 made the review screen render it as an error - on the one page that could have offered to build it.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | GoldSetView, nullable |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/gold-set
Section titled “POST /api/v1/filters/{filter_id}/gold-set”Build a gold set from the filter’s own source.
Reads a pool of events, proposes a label for each against the stated intent, and chooses about fifty for review. Minutes of work and a real inference bill, so this returns as soon as the row exists and the job is on the queue; GET on the same path follows it.
Refused while a build is already running, once a set is approved, and - unless replace_reviewed is set - when the current set already has reviewed items. A set built for an intent or source the filter no longer has is replaceable either way: it cannot be approved, so there is nothing of it left to protect.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/gold-set/approve
Section titled “POST /api/v1/filters/{filter_id}/gold-set/approve”Sign off the gold set, unlocking authoring -> shadow.
Approval is refused while any item is unreviewed. A gate that can be skipped is not a gate, and the disagreement rate is only meaningful over the whole set. It is refused too for a set built for an intent or source the filter no longer has, and for one already approved.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: ApproveGoldSetRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | GoldSetView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/gold-items/{item_id}/label
Section titled “POST /api/v1/gold-items/{item_id}/label”Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
item_id | path | yes | string |
Body: LabelGoldItemRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | GoldItemView |
422 | Validation Error | HTTPValidationError |
models
Section titled “models”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters/{filter_id}/models | admin, operator, reviewer | List Models |
| GET | /api/v1/filters/{filter_id}/threshold | admin, operator, reviewer | The operating point awaiting approval, if any. |
| POST | /api/v1/filters/{filter_id}/threshold/approve | admin, operator | Approve or reject a proposed operating point. |
| GET | /api/v1/filters/{filter_id}/threshold/decision | admin, operator, reviewer | The standing threshold decision for this filter’s current spec version. |
| POST | /api/v1/filters/{filter_id}/train | admin, operator | Queue a training job. |
| GET | /api/v1/models/{model_id} | admin, operator, reviewer | Get Model |
GET /api/v1/filters/{filter_id}/models
Section titled “GET /api/v1/filters/{filter_id}/models”Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of ModelSummary |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/threshold
Section titled “GET /api/v1/filters/{filter_id}/threshold”The operating point awaiting approval, if any.
Carries the recall measured on a temporal holdout, not an assumed one. That number is what the approver is actually being asked to accept. A rate budget’s is measured on ranked hours of shadow instead, and comes with them.
On an active filter it is a retrain waiting to replace the model running now, or nothing: the proposal names the model it would retire and carries the two compared on the same events.
A trained proposal a person rejected is not offered again: nothing older is offered in its place, and the next training proposes another (training.rejected_proposals).
A version waiting behind the running one comes first: the running model under that version, its own holdout re-scored through the new pipeline, with pending_version set. It is what the operator asked for, and a retrain approved meanwhile would leave it behind; the retrain is offered once it is decided. On a filter filtering through Collapse alone, a waiting retune of that stage (similarity, window or cluster-size cap) is offered on the stage’s own evidence instead: basis: "stage_evidence" with pending_version set, what it would fold away read off the running stage’s compression sweep at the new setting (a retune block in stage.evidence; a setting the sweep did not measure is unmeasured, never estimated), and its purity and count carried from the running settings. A retrain measured through a pipeline that no longer runs is not offered: after a swap, its figures describe the old pipeline, and the next retrain is measured through the new one.
Collapse first. On a filter not yet filtering, while no Decide stage filters - none, or one learning in shadow - the proposal is the stage approved on its own evidence (basis: "stage_evidence", stage): no Decide threshold, every survivor escalates, and its compression sweep and purity as the argument, once it has seen SIFT_COLLAPSE_MIN_VERIFIED_CLUSTERS clusters with verdicts. Below that floor, or before it is measured, nothing is proposed; nor, after a person rejected it, until its count has grown by a tenth of the floor (rounded up, at least one) or the stage’s settings have changed, which readiness’s Approve collapsing repeats step says with the count it comes back at. On an active filter filtering that way, a Decide stage learning behind it joins by the challenger path: its first trained model, measured through the running stages, with joins set and the running model named in replaces_model_id.
What each posture would pick. A trained proposal with a frontier carries posture_points: the point each named posture, and the spec’s custom multiple, would select from it, computed now. at reads the proposal at another point on its frontier - its figures, its saving and its comparison with what runs now - as an approval with threshold set to it would record them. A value off the frontier is 422 not_on_frontier; a proposal with none is 422 no_frontier.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
at | query | no | number, nullable |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ThresholdProposal, nullable |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/threshold/approve
Section titled “POST /api/v1/filters/{filter_id}/threshold/approve”Approve or reject a proposed operating point.
A typed rationale is required and lands in the audit log alongside the evidence the approver was shown. This is the only path by which a filter begins filtering anything, and the only one by which a running filter’s model is replaced.
On an active filter the proposal is a retrain waiting behind the model running now. Approving writes its operating point, records the approval and swaps the two models in one transaction, and the filter never leaves active. Rejecting keeps the running model and its approval, and retires the retrain.
Or it is a version waiting behind the running one (pending_version) - on a filter that is not active, one whose only gated change is to a stage that filters nothing (basis: "stage_change"), approved on that stage’s evidence by the same role, with no model and no operating point, its state unmoved. Approving records the approval against that version and swaps it in, in one transaction; the filter never leaves active. Rejecting discards it; the running version and its approval stand. That includes a retune of the Collapse stage a filter filters through alone (basis: "stage_evidence" with pending_version), whose swap moves no model: the fit already running clusters at the new setting. Where the sweep did not measure that setting, approving it needs accept_unmeasured_recall.
Every approval is written on the version it approves (approved_at, approved_by, in the same transaction), and the version history shows it. A rejection is recorded against no version and writes nothing. A rejected Collapse-only proposal is not offered again until its count has grown by a tenth of the floor or the stage’s settings change.
A rejected trained proposal is not offered again, where no model is running to keep instead: on a proposed filter, which moves back to shadow, a shadow one, or an active one filtering nothing. Its model stays as it is - in shadow it produces the filter’s shadow scores, so it is not retired - and the next training proposes another (training.rejected_proposals). Readiness then asks for training, not for this decision again.
Or, on an active filter that filters through Collapse alone, it is Decide’s first trained model offered to join (joins): approving swaps it in as a retrain is swapped, with the Decide stage set active in the same version, and needs accept_not_better unless it measured better than what runs now. Rejecting leaves the filter filtering through Collapse.
Or it is a stage proposed on its own evidence (basis: "stage_evidence"): a Collapse stage with no active Decide stage, offered once it has seen SIFT_COLLAPSE_MIN_VERIFIED_CLUSTERS clusters with verdicts. Approving takes the filter from shadow, proposed or paused into active through the gated edge, with Collapse filtering and no Decide threshold; the label floor does not apply, because nothing in it learns from labels.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: ApproveThresholdRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ThresholdProposal |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/threshold/decision
Section titled “GET /api/v1/filters/{filter_id}/threshold/decision”The standing threshold decision for this filter’s current spec version.
The record behind what the filter is doing, where GET /threshold is the thing awaiting a decision. On a filter that has been filtering for weeks the second is null and this one is the whole answer to who allowed this, and on what.
Read through filters_service.latest_threshold_decision, which is the row lifecycle._threshold_approved asks about when it decides whether the gate is open. One read, so the record and the rule cannot disagree - the same reason the gold set’s view takes its approver from the decision the gate reads rather than from the set’s own columns.
null when the current version has never been decided on, which is every filter before its first promotion.
A carried approval’s original approver is read through its chain (filters_service.original_approval), not from the row’s own evidence. In earlier releases the writer recorded the row it carried from, which from the second free save on is the previous saver’s carry. Approvals are append-only, so those rows cannot be corrected; reading through them is what makes them right.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ThresholdDecisionView, nullable |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/train
Section titled “POST /api/v1/filters/{filter_id}/train”Queue a training job.
Refused until the gold set is approved. That gate exists so a model is never fitted against an intent nobody has checked, and it is enforced here as well as in the task: the endpoint is not the only thing that enqueues training, and a gate that lives in one caller is not a gate.
Also refused, with the reason and what to do, when this install cannot train the deciding recipe: SetFit on an image without its extra. GET /system/recipes says so before anyone asks.
The resulting model enters shadow. It does not become active on its own metrics, however good the holdout looks.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: TrainRequest
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
GET /api/v1/models/{model_id}
Section titled “GET /api/v1/models/{model_id}”Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
model_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ModelSummary |
422 | Validation Error | HTTPValidationError |
operations
Section titled “operations”Read-only observation, including each filter’s Filtered events. A reviewer has to be able to see what was filtered; that visibility is the point of the record.
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters/{filter_id}/drift | admin, operator, reviewer | Recent drift readings. |
| GET | /api/v1/filters/{filter_id}/events | admin, operator, reviewer | The filter’s events, newest first, optionally those given one decision. |
| GET | /api/v1/filters/{filter_id}/filtered-events | admin, operator, reviewer | Every event this filter filtered, with score and model version. |
| GET | /api/v1/filters/{filter_id}/metrics | admin, operator, reviewer | What one filter did over the window, and what it missed. |
| GET | /api/v1/filters/{filter_id}/runs | admin, operator, reviewer | The filter’s agent runs, newest first. |
| GET | /api/v1/filters/{filter_id}/scores/histogram | admin, operator, reviewer | The Decide slot’s scores over the window, in 25 bins, with the threshold in force. |
| GET | /api/v1/filters/{filter_id}/timeline | admin, operator, reviewer | Everything that happened to this filter, newest first. |
GET /api/v1/filters/{filter_id}/drift
Section titled “GET /api/v1/filters/{filter_id}/drift”Recent drift readings.
A signal that could not be computed is recorded as an observation with measured false in its details, rather than omitted. “No drift” and “no data” are opposite conclusions and must not render the same.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of DriftObservation |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/events
Section titled “GET /api/v1/filters/{filter_id}/events”The filter’s events, newest first, optionally those given one decision.
delivery says how each reached the agent: single in a run of its own, grouped in a group run, held in a group Shape delivery has not released yet, split sent singly by the random split, with the delivery group’s id where one was recorded. null for an event that has not reached the agent.
rescued_by narrows to the events one always-escalate rule rescued, by its key; each rescued event carries the rule as rescued_by.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
decision | query | no | ScoreDecision, nullable | |
rescued_by | query | no | string, nullable | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_EventView_ |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/filtered-events
Section titled “GET /api/v1/filters/{filter_id}/filtered-events”Every event this filter filtered, with score and model version.
No payloads: the log records that a decision was made and by which model, not a second copy of the customer’s data. It is retained longer than event payloads so a newly discovered threat pattern can still be checked against it, which is why searching by source event id matters more here than anywhere else in the API.
Only what the filter filtered, unless asked. The log also records what reached no agent for another cause - an agent budget’s overflow, sampled shadow’s undrawn events - and those are not filtered: cause reads them, one cause at a time, so no reader of this endpoint counts them as the filter’s decision.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
source_event_id | query | no | string, nullable | |
start | query | no | string, nullable | |
end | query | no | string, nullable | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
cause | query | no | FilterCause | Why the events did not reach the agent. filter (the default) is what the filter filtered; over_budget what its agent budget kept back, and not_sampled what sampled shadow did not draw, which are never filtered. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_FilteredEventView_ |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/metrics
Section titled “GET /api/v1/filters/{filter_id}/metrics”What one filter did over the window, and what it missed.
Runs avoided and spend saved, and alongside them the measured false-negative rate from leak-through and its limit. Both halves are returned together on purpose: the savings figure alone is only half the decision. The window before comes with them (previous), and so do the busiest hour against the ceiling, how the sends ended, the time to a verdict, the pitch, and the last day’s polls.
Runs avoided counts filtered events only, and spend saved prices them; neither includes Shape delivery’s grouping. Grouping’s saving is runs_saved_by_grouping (null for a filter that has never released a group, which is not zero) and estimated_spend_saved_by_grouping, at the measured cost of a group run from the newest analysis of the filter’s agent that saw group runs, or else as an upper bound (grouping_spend_basis). agent_runs_saved adds the two run figures. escalations_delivered is in events and agent_runs in runs; the busiest hour counts escalations, as Max escalations per hour does.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
window_days | query | no | integer | Default 7. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterCostMetrics |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/runs
Section titled “GET /api/v1/filters/{filter_id}/runs”The filter’s agent runs, newest first.
A run that carried a group of escalations (Shape delivery) has event_id null and its events in group: the group’s id, its size, and its members’ event ids in slot order. In earlier releases such a run read event_id: "" and had no group; legacy_event_id=true keeps the empty string for one release.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 50. |
legacy_event_id | query | no | boolean | Deprecated, for one release: true reports a group run’s event_id as "", as before, rather than null. Default False. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Page_AgentRunView_ |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/scores/histogram
Section titled “GET /api/v1/filters/{filter_id}/scores/histogram”The Decide slot’s scores over the window, in 25 bins, with the threshold in force.
Only the events that reached Decide, so the counts either side of the threshold are what Decide filtered and sent. A filter whose Decide is a fixed rule, a rate budget, or absent has no histogram, and absent_because says which.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
window_days | query | no | integer | Default 7. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | ScoreHistogram |
422 | Validation Error | HTTPValidationError |
GET /api/v1/filters/{filter_id}/timeline
Section titled “GET /api/v1/filters/{filter_id}/timeline”Everything that happened to this filter, newest first.
Merged from the records that already exist - polls, agent runs and their verdicts, the audit log, training jobs, drift breaches - rather than kept as a copy. Named timeline because /events is the list of source events.
types narrows to some kinds (repeat the parameter). quiet_polls decides what happens to polls that found nothing new: collapse (default) folds a run of them into one entry, exclude drops them, include shows each. since and until are inclusive.
event_id (Sift’s id for one of this filter’s events) narrows the page to that event: its runs, sends and verdicts, its own or those of the delivery group it was held or delivered in, with event saying how it was delivered.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
types | query | no | array of TimelineKind, nullable | |
since | query | no | string, nullable | |
until | query | no | string, nullable | |
quiet_polls | query | no | QuietPolls | Default collapse. |
cursor | query | no | string, nullable | Opaque cursor from a previous response’s next_cursor. |
limit | query | no | integer | Default 100. |
event_id | query | no | string, nullable |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | TimelinePage |
422 | Validation Error | HTTPValidationError |
activity
Section titled “activity”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/activity | admin, operator, reviewer | What every filter did, day by day, plus the trailing miss rate. |
| GET | /api/v1/activity/filters | admin, operator, reviewer | One series per named filter, for a page of list panels. |
| GET | /api/v1/activity/filters/{filter_id} | admin, operator, reviewer | One filter, day by day, for its own page. |
| GET | /api/v1/activity/filters/{filter_id}/flow | admin, operator, reviewer | One filter’s window, as the paths its events took from the stream to the verdict. |
| GET | /api/v1/activity/flow | admin, operator, reviewer | Every filter in view, as the paths its events took, and the paths summed. |
GET /api/v1/activity
Section titled “GET /api/v1/activity”What every filter did, day by day, plus the trailing miss rate.
The miss rate is its own series because it carries its own sample size, and the sample size is what decides whether a point may be drawn at all. Below the floor the rate is null and only the sample is reported - a gap in the line, never a zero in it.
Each day counts escalations_delivered in events, beside filtered and would_filter, and agent_runs in runs, which under Shape delivery carry many escalations each. sent is agent_runs under its old name, deprecated and kept for this release only.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | PortfolioActivity |
422 | Validation Error | HTTPValidationError |
GET /api/v1/activity/filters
Section titled “GET /api/v1/activity/filters”One series per named filter, for a page of list panels.
Unknown ids are dropped rather than raising. The caller is a list page rendering what it already loaded, and a filter retired between the two calls must not blank the whole page.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
ids | query | no | array of string | |
window_days | query | no | integer | Default 7. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterActivityBatch |
422 | Validation Error | HTTPValidationError |
GET /api/v1/activity/filters/{filter_id}
Section titled “GET /api/v1/activity/filters/{filter_id}”One filter, day by day, for its own page.
As GET /activity: escalations_delivered in events, agent_runs in runs, and sent, deprecated, the runs under their old name for this release only.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
window_days | query | no | integer | Default 7. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterActivity |
422 | Validation Error | HTTPValidationError |
GET /api/v1/activity/filters/{filter_id}/flow
Section titled “GET /api/v1/activity/filters/{filter_id}/flow”One filter’s window, as the paths its events took from the stream to the verdict.
Read on what it did when it filters, on its proposal when one awaits approval in shadow or proposed, and on its recorded counterfactual otherwise (basis). Asked for basis=actual, it reads what happened whatever the state: a proposal’s holdout has no sends, and what became of the window’s sends does not depend on what the filter would do.
The flow stays in events. Where a group run was released in the window, runs says how many runs carried the events sent, an annotation on the sent node and never a node events flow into (null otherwise). Events Shape delivery still holds, and those in flight, are their own nodes, not counted as sent.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
window_days | query | no | integer | Default 30. |
basis | query | no | string, nullable | actual reads what happened, whatever the filter’s state: what became of what was sent, for Ingest and delivery. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FilterFlow |
422 | Validation Error | HTTPValidationError |
GET /api/v1/activity/flow
Section titled “GET /api/v1/activity/flow”Every filter in view, as the paths its events took, and the paths summed.
“In view” is :mod:~sift.services.visibility’s, as /fleet’s rows are. A filter that filters is read on what it did, and every other on its recorded counterfactual: the window’s own traffic, as the dashboard’s would filter counts it. Never on a proposal, whose flow is its holdout, its model’s traffic since training or its budget’s ranked hours, none of them this window’s events; a proposal’s picture is on its filter’s Overview, beside its approval. The totals keep what a filter would have filtered apart from what one did.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FleetFlow |
422 | Validation Error | HTTPValidationError |
alerts
Section titled “alerts”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/alerts | admin, operator, reviewer | Every filter with a fault or something outstanding, worst first and uncapped. |
GET /api/v1/alerts
Section titled “GET /api/v1/alerts”Every filter with a fault or something outstanding, worst first and uncapped.
The health verdict is carried verbatim, so this list, the dot on a filter panel and the recommended action on a filter’s own page are three renderings of one derivation rather than three opinions.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | AlertsSummary |
dashboard
Section titled “dashboard”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/asks | admin, operator, reviewer | Every filter in view with a condition or a recommendation, worst first. |
| GET | /api/v1/dashboard | admin, operator, reviewer | Aggregates across every filter over a trailing window. |
| GET | /api/v1/decisions | admin, operator, reviewer | The latest twenty state changes and approvals over the window, in the timeline’s words, each naming its filter. |
GET /api/v1/asks
Section titled “GET /api/v1/asks”Every filter in view with a condition or a recommendation, worst first.
Each filter’s asks are its own page’s, from the same builder, so the first condition is its health verdict: what the bell and the Alerts page say about it.
Requires: admin, operator, reviewer
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | FleetAsks |
GET /api/v1/dashboard
Section titled “GET /api/v1/dashboard”Aggregates across every filter over a trailing window.
Pooled rather than averaged: the false-negative rate is computed from every leak-through label together, so a quiet filter with three labels cannot weigh as much as a busy one with three thousand.
Escalations and runs are two units, never added or subtracted: escalations_delivered counts events, agent_runs counts runs, and under Shape delivery one run carries many escalations. sent is agent_runs under its old name, deprecated and kept for this release only. agent_runs_avoided and estimated_spend_saved count filtered events only. Grouping’s saving is apart: runs_saved_by_grouping, the events in the group runs released less those runs, read from what was released and null while no filter has ever grouped; and estimated_spend_saved_by_grouping, priced at the measured cost of a group run or as an upper bound, which grouping_spend_basis says. agent_runs_saved is the two run figures added, for showing beside its parts.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | DashboardSummary |
422 | Validation Error | HTTPValidationError |
GET /api/v1/decisions
Section titled “GET /api/v1/decisions”The latest twenty state changes and approvals over the window, in the timeline’s words, each naming its filter.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | RecentDecisions |
422 | Validation Error | HTTPValidationError |
| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/fleet | admin, operator, reviewer | Every filter in view, one row each, worst first. |
GET /api/v1/fleet
Section titled “GET /api/v1/fleet”Every filter in view, one row each, worst first.
Sorted by attention_rank, then by events ingested, most first; retired filters last. Each row’s figures are the ones the filter’s own metrics and activity report for the same window, read by the same functions over every filter at once.
runs_saved_by_grouping is the Grouped column, apart from agent_runs_avoided and null for a filter that has never grouped; escalations_delivered is in events and agent_runs in runs, with sent the runs under their old name, deprecated for this release only.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
window_days | query | no | integer | Default 30. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of FleetRow |
422 | Validation Error | HTTPValidationError |
labels
Section titled “labels”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters/{filter_id}/labels | admin, operator, reviewer | What training would count for this filter, by source, and its agent’s history. |
| POST | /api/v1/filters/{filter_id}/labels/history | admin, operator | Train on the past verdicts of the agent this filter was built from. |
| DELETE | /api/v1/filters/{filter_id}/labels/history | admin, operator | Remove every historical label from this filter’s training set. |
GET /api/v1/filters/{filter_id}/labels
Section titled “GET /api/v1/filters/{filter_id}/labels”What training would count for this filter, by source, and its agent’s history.
The count every other screen shows - the Actions tab, the list’s health, the filter detail - broken down: reviewed gold items, the filter’s own agent, and historical verdicts, with the ones held but not counted said separately.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | TrainingLabels |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/labels/history
Section titled “POST /api/v1/filters/{filter_id}/labels/history”Train on the past verdicts of the agent this filter was built from.
Reads that agent’s runs from Kindo again - up to max_runs, within the labelled payload window - and keeps the ones with a verdict, a time, an input carrying a field the filter reads, and no run Sift itself triggered. They count toward the label floor only until the filter’s own labels reach it.
Refused when there is no such agent, for a retired filter, and while an import is already running.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
202 | Successful Response | JobAccepted |
422 | Validation Error | HTTPValidationError |
DELETE /api/v1/filters/{filter_id}/labels/history
Section titled “DELETE /api/v1/filters/{filter_id}/labels/history”Remove every historical label from this filter’s training set.
The next training run fits without them. Nothing else is touched, and they can be imported again.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | TrainingLabels |
422 | Validation Error | HTTPValidationError |
recipes
Section titled “recipes”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/admin/recipes | admin | Every recipe in catalog order, with all three answers and who last decided. |
| POST | /api/v1/admin/recipes/{recipe_id}/enabled | admin | Turn a recipe on or off for new filters. |
GET /api/v1/admin/recipes
Section titled “GET /api/v1/admin/recipes”Every recipe in catalog order, with all three answers and who last decided.
In catalog order rather than by state, because that order is the one an operator meets recipes in everywhere else in the product, and a screen that re-sorts by whatever is broken today teaches a different set each time it is opened.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | RecipeSettingsOverview |
POST /api/v1/admin/recipes/{recipe_id}/enabled
Section titled “POST /api/v1/admin/recipes/{recipe_id}/enabled”Turn a recipe on or off for new filters.
Refused for a recipe this install cannot train: the screen offers no control in that state, and turning on what cannot run would put a promise on the screen that the deployment cannot keep. A request that changes nothing writes nothing, not even an audit row, as users.change_role does.
Answers with the whole section rather than the one row, because turning a recipe off is the kind of change an operator reads in context - what is left available - and it saves the screen a second round trip to find out.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
recipe_id | path | yes | RecipeId |
Body: SetRecipeEnabledRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | RecipeSettingsOverview |
422 | Validation Error | HTTPValidationError |
verdict-check
Section titled “verdict-check”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/filters/{filter_id}/verdict-check | admin, operator, reviewer | Where the filter’s verdict check stands, and its queue. |
| PUT | /api/v1/filters/{filter_id}/verdict-check | admin, operator | Switch the verdict check on or off for this filter. |
| POST | /api/v1/filters/{filter_id}/verdict-check/items/{item_id}/answer | admin, operator, reviewer | A person’s answer to one item. |
| POST | /api/v1/filters/{filter_id}/verdict-check/resume | admin, operator | Resume a check its stop rule paused. Its count of answers starts again. |
GET /api/v1/filters/{filter_id}/verdict-check
Section titled “GET /api/v1/filters/{filter_id}/verdict-check”Where the filter’s verdict check stands, and its queue.
Every open item, oldest first, then the newest answered ones. Each carries the event as it arrived and the agent’s final message, both untrusted text, and the model’s reading as a word. items=false serves the standing alone. Asks no model.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
items | query | no | boolean | Default True. |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | VerdictCheckView |
422 | Validation Error | HTTPValidationError |
PUT /api/v1/filters/{filter_id}/verdict-check
Section titled “PUT /api/v1/filters/{filter_id}/verdict-check”Switch the verdict check on or off for this filter.
A filter setting, not a spec change: it changes nothing the filter decides. On is refused (409) unless the decision model reads the filter well, and for a mock filter. It checks verdicts written from now on. Off keeps every item and answer.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
Body: VerdictCheckSwitch
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | VerdictCheckView |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/verdict-check/items/{item_id}/answer
Section titled “POST /api/v1/filters/{filter_id}/verdict-check/items/{item_id}/answer”A person’s answer to one item.
The agent was wrong writes the event’s analyst label with the opposite verdict; right writes one with the agent’s; can’t tell writes none. Training uses the person’s label over the agent’s. An answer can be changed until the event’s payload is purged (409). Asks no model.
Requires: admin, operator, reviewer
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string | |
item_id | path | yes | string |
Body: VerdictCheckAnswerRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | VerdictCheckItem |
422 | Validation Error | HTTPValidationError |
POST /api/v1/filters/{filter_id}/verdict-check/resume
Section titled “POST /api/v1/filters/{filter_id}/verdict-check/resume”Resume a check its stop rule paused. Its count of answers starts again.
Requires: admin, operator
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
filter_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | VerdictCheckView |
422 | Validation Error | HTTPValidationError |
webhooks
Section titled “webhooks”| Method | Path | Requires | Summary |
|---|---|---|---|
| GET | /api/v1/admin/webhooks | admin | Every endpoint, its last delivery, and the rules they are all sent under. |
| POST | /api/v1/admin/webhooks | admin | Save an endpoint, after the URL passes the egress check. |
| PATCH | /api/v1/admin/webhooks/{endpoint_id} | admin | Change an endpoint. A new URL passes the egress check again; the secret rotates through rotate-secret instead. |
| DELETE | /api/v1/admin/webhooks/{endpoint_id} | admin | Delete it. What it had queued is abandoned; its delivery log keeps its name. |
| GET | /api/v1/admin/webhooks/{endpoint_id}/deliveries | admin | The last 100 deliveries, newest first, with attempts and errors. |
| POST | /api/v1/admin/webhooks/{endpoint_id}/rotate-secret | admin | A new signing secret. For a day, every delivery carries a signature under both. |
| POST | /api/v1/admin/webhooks/{endpoint_id}/test | admin | Send a webhook.test now, outside the queue, once, and say what came back. |
GET /api/v1/admin/webhooks
Section titled “GET /api/v1/admin/webhooks”Every endpoint, its last delivery, and the rules they are all sent under.
Requires: admin
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | WebhookOverview |
POST /api/v1/admin/webhooks
Section titled “POST /api/v1/admin/webhooks”Save an endpoint, after the URL passes the egress check.
The check resolves the host now, and again before every delivery: a URL that resolves anywhere a webhook may not go is refused here with the reason.
Requires: admin
Body: CreateWebhookRequest
| Response | Meaning | Schema |
|---|---|---|
201 | Successful Response | WebhookEndpointSummary |
422 | Validation Error | HTTPValidationError |
PATCH /api/v1/admin/webhooks/{endpoint_id}
Section titled “PATCH /api/v1/admin/webhooks/{endpoint_id}”Change an endpoint. A new URL passes the egress check again; the secret rotates through rotate-secret instead.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
endpoint_id | path | yes | string |
Body: UpdateWebhookRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | WebhookEndpointSummary |
422 | Validation Error | HTTPValidationError |
DELETE /api/v1/admin/webhooks/{endpoint_id}
Section titled “DELETE /api/v1/admin/webhooks/{endpoint_id}”Delete it. What it had queued is abandoned; its delivery log keeps its name.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
endpoint_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | Ack |
422 | Validation Error | HTTPValidationError |
GET /api/v1/admin/webhooks/{endpoint_id}/deliveries
Section titled “GET /api/v1/admin/webhooks/{endpoint_id}/deliveries”The last 100 deliveries, newest first, with attempts and errors.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
endpoint_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | array of WebhookDeliverySummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/admin/webhooks/{endpoint_id}/rotate-secret
Section titled “POST /api/v1/admin/webhooks/{endpoint_id}/rotate-secret”A new signing secret. For a day, every delivery carries a signature under both.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
endpoint_id | path | yes | string |
Body: RotateWebhookSecretRequest
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | WebhookEndpointSummary |
422 | Validation Error | HTTPValidationError |
POST /api/v1/admin/webhooks/{endpoint_id}/test
Section titled “POST /api/v1/admin/webhooks/{endpoint_id}/test”Send a webhook.test now, outside the queue, once, and say what came back.
A receiver that refused it answers 200 here with delivered: false and the reason: the question was whether it works, and that is the answer.
Requires: admin
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
endpoint_id | path | yes | string |
| Response | Meaning | Schema |
|---|---|---|
200 | Successful Response | WebhookTestResult |
422 | Validation Error | HTTPValidationError |