Skip to content

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.

Unauthenticated. /healthz is liveness, /readyz checks Postgres, Redis, and object storage and is the one an orchestrator should gate on.

MethodPathRequiresSummary
GET/healthzpublicHealthz
GET/readyzpublicReadyz

Requires: public

ResponseMeaningSchema
200Successful ResponseHealthResponse

Requires: public

ResponseMeaningSchema
200Successful ResponseReadyResponse

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.

MethodPathRequiresSummary
POST/api/v1/auth/change-passwordadmin, operator, reviewerReplace the current password.
POST/api/v1/auth/loginpublicExchange credentials for a session cookie.
POST/api/v1/auth/logoutadmin, operator, reviewerEnd the session on the server, not only in this browser.
GET/api/v1/auth/meadmin, operator, reviewerThe current session.

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

ResponseMeaningSchema
200Successful ResponseSessionResponse
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseSessionResponse
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseAck

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

ResponseMeaningSchema
200Successful ResponseSessionResponse
MethodPathRequiresSummary
GET/api/v1/system/decisionsadmin, operator, reviewerThe decision model this deployment asks, whether it is answering, and its usage.
POST/api/v1/system/decisions/pingadminAsk the decision model one fixed question now, and say what came back.
GET/api/v1/system/kindoadmin, operator, reviewerWhether Sift is reaching the Kindo tenant, from the calls it already makes.
POST/api/v1/system/pingadmin, operatorDispatch a task and wait for its result.
GET/api/v1/system/recipesadmin, operator, reviewerWhich recipes this deployment can build a filter on, and why not where it cannot.
GET/api/v1/system/slotsadmin, operator, reviewerThe pipeline’s slots, in order, and which recipes this deployment can run in each.
GET/api/v1/system/spec-rulesadmin, operator, reviewerWhich way each spec change needs approval, before anything is saved.
GET/api/v1/system/statusadmin, operator, reviewerService identity plus anything an operator should act on.

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

ResponseMeaningSchema
200Successful ResponseDecisionModelStatus

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

ResponseMeaningSchema
200Successful ResponseDecisionPing

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

ResponseMeaningSchema
200Successful ResponseKindoConnection

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

ParameterInRequiredTypeNotes
queuequerynoio | ml | notifyDefault io.
echoquerynostringDefault pong.
ResponseMeaningSchema
200Successful ResponsePingResult
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseRecipeAvailabilityReport

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

ResponseMeaningSchema
200Successful ResponseSlotsReport

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

ResponseMeaningSchema
200Successful ResponseSpecRules

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

ResponseMeaningSchema
200Successful ResponseSystemStatus
MethodPathRequiresSummary
GET/api/v1/audit-logadminEvery state transition, with actor and rationale.
GET/api/v1/usersadminList Users
POST/api/v1/usersadminCreate User
POST/api/v1/users/{user_id}/disableadminStop an account signing in, and end every session it has open.
POST/api/v1/users/{user_id}/enableadminLet a disabled account sign in again, with its existing password.
POST/api/v1/users/{user_id}/roleadminGive an account a different role.

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

ParameterInRequiredTypeNotes
actor_idquerynostring, nullable
target_typequerynostring, nullable
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_AuditEntry_
422Validation ErrorHTTPValidationError

Requires: admin

ResponseMeaningSchema
200Successful Responsearray of UserSummary

Requires: admin

Body: CreateUserRequest

ResponseMeaningSchema
201Successful ResponseUserSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
user_idpathyesstring
ResponseMeaningSchema
200Successful ResponseUserSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
user_idpathyesstring
ResponseMeaningSchema
200Successful ResponseUserSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
user_idpathyesstring

Body: ChangeRoleRequest

ResponseMeaningSchema
200Successful ResponseUserSummary
422Validation ErrorHTTPValidationError

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.

MethodPathRequiresSummary
GET/api/v1/credentialsadminThe live connection and the key staged beside it; either may be null.
GET/api/v1/credentials/checks/{check_id}adminOne check of either key, as far as it has got. Every 202 here names this.
POST/api/v1/credentials/pendingadminWizard step 2: connect with a new key and stage it, or refuse it with Kindo’s reason.
DELETE/api/v1/credentials/pendingadminCancel: delete the staged key and its checks. The live connection is untouched.
GET/api/v1/credentials/pending/catalogadminThe staged key’s catalog, re-read through the staged key: the model step after a reload.
GET/api/v1/credentials/pending/checksadminThe staged key’s latest check: what the wizard resumes from.
POST/api/v1/credentials/pending/checksadminWizard step 4: run the staged key’s checks with the chosen model.
POST/api/v1/credentials/pending/promoteadminWizard step 5: confirm a replacement, and the staged key becomes the connection.
DELETE/api/v1/credentials/{credential_id}adminDelete Credential
POST/api/v1/credentials/{credential_id}/model/checksadminChange model: test a model on the live key, and store it when it passes.
POST/api/v1/credentials/{credential_id}/verifyadminVerify: the tenant, the stored model and every paired agent, on the live key.

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

ResponseMeaningSchema
200Successful ResponseCredentialConnection

One check of either key, as far as it has got. Every 202 here names this.

Requires: admin

ParameterInRequiredTypeNotes
check_idpathyesstring
ResponseMeaningSchema
200Successful ResponseCredentialCheckView
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
201Successful ResponseStagedCredential
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseAck

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

ResponseMeaningSchema
200Successful ResponseModelCatalog

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

ResponseMeaningSchema
200Successful ResponseCredentialCheckView

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

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseCredentialSummary

DELETE /api/v1/credentials/{credential_id}

Section titled “DELETE /api/v1/credentials/{credential_id}”

Requires: admin

ParameterInRequiredTypeNotes
credential_idpathyesstring
ResponseMeaningSchema
200Successful ResponseAck
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
credential_idpathyesstring

Body: ModelCheckRequest

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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 is no longer offered to this key and raises kindo_model_unavailable until another is chosen. 409 while a check of this key is running.

Requires: admin

ParameterInRequiredTypeNotes
credential_idpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/kindo/agentsadmin, operator, reviewerOne page of the tenant’s agents, with the upstream cursor passed straight through.
GET/api/v1/kindo/agents/{agent_id}admin, operator, reviewerOne agent, by id.
GET/api/v1/kindo/integrationsadmin, operator, reviewerThe 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}/checkadmin, operatorWhether Sift can read this integration, before any conversation starts.
GET/api/v1/kindo/modelsadmin, operator, reviewerThe live key’s model catalog, grouped as every model selector shows it.

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

ParameterInRequiredTypeNotes
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_KindoAgentSummary_
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
agent_idpathyesstring
ResponseMeaningSchema
200Successful ResponseKindoAgentDetail
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful Responsearray 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

ParameterInRequiredTypeNotes
slugpathyesstring
forquerynostringDefault “.
ResponseMeaningSchema
200Successful ResponseSourceCheck
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseModelCatalog

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.

MethodPathRequiresSummary
GET/api/v1/analysisadmin, operatorList Reports
POST/api/v1/analysis/agentsadmin, operatorQueue an analysis of one agent’s run history.
POST/api/v1/analysis/portfolio-scanadmin, operatorQueue a ranking of every agent by filtering opportunity.
GET/api/v1/analysis/portfolio-scan/{scan_id}admin, operatorOne portfolio scan and its ranking, as far as it has got.
GET/api/v1/analysis/{report_id}admin, operatorOne report.

Requires: admin, operator

ParameterInRequiredTypeNotes
agent_idquerynostring, nullable
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_AnalysisReport_
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
scan_idpathyesstring
ResponseMeaningSchema
200Successful ResponsePortfolioScan
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
report_idpathyesstring
ResponseMeaningSchema
200Successful ResponseAnalysisReport
422Validation ErrorHTTPValidationError

The copilot proposes configuration and cannot apply it. It holds no write tools by design.

MethodPathRequiresSummary
POST/api/v1/copilot/intakeadmin, operatorRead what the operator typed, and either refuse it or say where it lands.
GET/api/v1/copilot/sessionsadmin, operatorThe caller’s unexpired conversations, newest change first.
POST/api/v1/copilot/sessionsadmin, operatorOpen a conversation.
GET/api/v1/copilot/sessions/{session_id}admin, operatorGet Session
POST/api/v1/copilot/sessions/{session_id}/analysisadmin, operatorHand a finished analysis to a conversation already under way.
POST/api/v1/copilot/sessions/{session_id}/deliver-replayadmin, operatorWhat Shape delivery would save on the session’s last sample.
POST/api/v1/copilot/sessions/{session_id}/messagesadmin, operatorSend a message and stream the reply as server-sent events.
GET/api/v1/copilot/sessions/{session_id}/sweepadmin, operatorThe session’s last sample as the review step reads it, and its latest sweep.
POST/api/v1/copilot/sessions/{session_id}/sweepadmin, operatorSweep Collapse’s similarity settings over the session’s last sample.
GET/api/v1/copilot/starters/{slug}admin, operatorWays to start a filter over one integration, before anyone has typed anything.

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

ResponseMeaningSchema
200Successful ResponseIntakeResult
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
minequerynobooleanMust be true: a caller lists their own sessions, and only those. Default True.
ResponseMeaningSchema
200Successful ResponseCopilotSessionList
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
201Successful ResponseCopilotSession
422Validation ErrorHTTPValidationError

Requires: admin, operator

ParameterInRequiredTypeNotes
session_idpathyesstring
ResponseMeaningSchema
200Successful ResponseCopilotSession
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
session_idpathyesstring

Body: AttachAnalysisRequest

ResponseMeaningSchema
200Successful ResponseCopilotSession
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
session_idpathyesstring

Body: SampleDeliverReplayRequest

ResponseMeaningSchema
200Successful ResponseSampleDeliverReplay
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
session_idpathyesstring

Body: SendMessageRequest

ResponseMeaningSchema
200Successful Responseany
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
session_idpathyesstring
ResponseMeaningSchema
200Successful ResponseSampleSweepView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
session_idpathyesstring

Body: SampleSweepRequest

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
slugpathyesstring
ResponseMeaningSchema
200Successful ResponseStarterSet
422Validation ErrorHTTPValidationError

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.

MethodPathRequiresSummary
GET/api/v1/filtersadmin, operator, reviewerA page of filters, newest first, optionally narrowed to some states.
POST/api/v1/filtersadmin, operatorCreate a filter: in authoring with a spec, in draft without one.
POST/api/v1/filters/load-previewadmin, operator, reviewerThe volume preflight for a draft, before a filter exists.
GET/api/v1/filters/{filter_id}admin, operator, reviewerGet Filter
POST/api/v1/filters/{filter_id}/asks/{code}/declineadmin, operatorDecline a recommendation: not on this evidence, with a reason.
GET/api/v1/filters/{filter_id}/bakeoffadmin, operator, reviewerWhat each recipe on the slate has shown, and which one has the best argument.
PUT/api/v1/filters/{filter_id}/candidatesadmin, operatorReplace Decide’s slate: PUT .../stages/decide/candidates, kept for one release.
POST/api/v1/filters/{filter_id}/cutover/acknowledgeadmin, operatorAcknowledge that the agent this filter’s pairing displaced may keep running.
GET/api/v1/filters/{filter_id}/decision-fitadmin, operator, reviewerHow 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/measureadmin, operatorTake a fit reading now, on the io worker (Measure now).
GET/api/v1/filters/{filter_id}/fieldsadmin, operator, reviewerThe field paths the filter’s recent events carry, for a field picker.
GET/api/v1/filters/{filter_id}/load-previewadmin, operator, reviewerThe volume preflight for this filter’s current spec.
POST/api/v1/filters/{filter_id}/pairadmin, operatorConfirm that agent_id is the agent this filter feeds.
POST/api/v1/filters/{filter_id}/pair/previewadmin, operatorWhat pairing agent_id would do, without pairing it.
POST/api/v1/filters/{filter_id}/paired-agentadmin, operatorThe agent built to consume this filter’s output, ready to import.
POST/api/v1/filters/{filter_id}/paired-agent/createadmin, operatorCreate the generated agent in Kindo and pair this filter to it.
POST/api/v1/filters/{filter_id}/paired-agent/create/previewadmin, operatorWhat Create in Kindo and connect would do to the filter, before anything exists.
POST/api/v1/filters/{filter_id}/pauseadmin, operatorStop reading the source: nothing is filtered, and no agent run is started.
POST/api/v1/filters/{filter_id}/restore-pointadmin, operatorReturn a filter at its safer point to its approved point.
POST/api/v1/filters/{filter_id}/resumeadmin, operatorFilter again on the model that was running, re-affirming its approval.
POST/api/v1/filters/{filter_id}/stages/guarantee/tryadmin, operatorTry it: ask one always-escalate rule of up to 200 recent filtered events.
PUT/api/v1/filters/{filter_id}/stages/{slot}admin, operatorSet the stage in a slot (id, params, mode), or remove it with null.
PUT/api/v1/filters/{filter_id}/stages/{slot}/candidatesadmin, operatorReplace the slate of a slot whose stage carries candidates. Touches no stage.
GET/api/v1/filters/{filter_id}/stages/{slot}/evidenceadmin, operator, reviewerEvery evidence block the stage in a slot reads, in full, each with its kind.
GET/api/v1/filters/{filter_id}/stages/{slot}/examplesadmin, operator, reviewerExamples of what the stage in a slot did over its window, largest group first.
POST/api/v1/filters/{filter_id}/stages/{slot}/measureadmin, operatorRun the stage’s evidence now, on the ML worker.
POST/api/v1/filters/{filter_id}/stages/{slot}/previewadmin, operator, reviewerWhat PUT .../stages/{slot} with this stage would do, without saving it.
POST/api/v1/filters/{filter_id}/stages/{slot}/winneradmin, operatorPromote a candidate within its slot: it becomes that stage’s recipe, active.
POST/api/v1/filters/{filter_id}/stateadmin, operatorMove a filter through the lifecycle.
GET/api/v1/filters/{filter_id}/verdict-briefingadmin, operator, reviewerThe verdict contract, as text to paste into an agent Sift did not write.
GET/api/v1/filters/{filter_id}/versionsadmin, operator, reviewerEvery 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}/versionsadmin, operatorRecord a new spec version.
POST/api/v1/filters/{filter_id}/versions/pending/measureadmin, operatorQueue the waiting version’s re-measurement again.
POST/api/v1/filters/{filter_id}/versions/previewadmin, operator, reviewerWhat saving this spec would do, without saving it.
GET/api/v1/filters/{filter_id}/warm-startadmin, operator, reviewerWhether a warm start is offered for this filter now, and the latest one.
POST/api/v1/filters/{filter_id}/warm-startadmin, operatorHave the paired agent judge events events of this filter’s history.
POST/api/v1/filters/{filter_id}/winneradmin, operatorMake a candidate Decide’s recipe: POST .../stages/decide/winner, for one release.

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

ParameterInRequiredTypeNotes
statequerynoarray of FilterState, nullable
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_FilterSummary_
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
201Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ResponseMeaningSchema
200Successful ResponseLoadPreview
422Validation ErrorHTTPValidationError

Requires: admin, operator, reviewer

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
codepathyesstring

Body: DeclineRecommendationRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseBakeoffView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: UpdateCandidatesRequest

ResponseMeaningSchema
200Successful ResponseBakeoffView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: AcknowledgeCutoverRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseDecisionFitView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseEventFieldsView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
statedquerynounder_1k | 1k_10k | 10k_100k | over_100k | custom, nullableWhat the operator said the stream is: a preset, or custom.
stated_events_per_hourquerynonumber, nullableWith stated=custom, and only with it.
ResponseMeaningSchema
200Successful ResponseLoadPreview
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: PairAgentRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: PairAgentRequest

ResponseMeaningSchema
200Successful ResponsePairPreview
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: PairedAgentRequest

ResponseMeaningSchema
200Successful ResponsePairedAgentDefinition
422Validation ErrorHTTPValidationError

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:

  1. Everything /pair/preview would 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.
  2. The definition is the one the download serves, with a fresh sift-create: marker as the last line of its description.
  3. 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.
  4. The agent is read back once, and filter.agent_created is 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 in details.created_agent_id so the console can offer to pair to it.
  5. The pairing /pair runs, 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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: CreatePairedAgentRequest

ResponseMeaningSchema
200Successful ResponseCreatedPairing
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponsePairPreview
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: RestorePointRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: ResumeRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: GuaranteeTryRequest

ResponseMeaningSchema
200Successful ResponseGuaranteeTryResult
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring

Body: SetStageRequest

ResponseMeaningSchema
200Successful ResponseStageWriteResult
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring

Body: UpdateCandidatesRequest

ResponseMeaningSchema
200Successful ResponseBakeoffView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring
ResponseMeaningSchema
200Successful ResponseStageEvidenceView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring
ResponseMeaningSchema
200Successful ResponseStageExamplesView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring

Body: PreviewStageRequest

ResponseMeaningSchema
200Successful ResponseSpecChangePreview
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
slotpathyesstring

Body: SelectWinnerRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: ChangeStateRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseVerdictBriefing
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_FilterVersionSummary_
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: CreateFilterVersionRequest

ResponseMeaningSchema
201Successful ResponseFilterVersionSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: PreviewFilterVersionRequest

ResponseMeaningSchema
200Successful ResponseSpecChangePreview
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseWarmStartReading
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: WarmStartRequest

ResponseMeaningSchema
202Successful ResponseWarmStartView
422Validation ErrorHTTPValidationError

Make a candidate Decide’s recipe: POST .../stages/decide/winner, for one release.

See that endpoint; the same refusals apply.

Requires: admin, operator

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: SelectWinnerRequest

ResponseMeaningSchema
200Successful ResponseFilterDetail
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/filters/{filter_id}/gold-setadmin, operator, reviewerThe filter’s latest gold set, or null when it has never had one.
POST/api/v1/filters/{filter_id}/gold-setadmin, operatorBuild a gold set from the filter’s own source.
POST/api/v1/filters/{filter_id}/gold-set/approveadmin, operatorSign off the gold set, unlocking authoring -> shadow.
POST/api/v1/gold-items/{item_id}/labeladmin, operator, reviewerLabel Gold Item

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseGoldSetView, nullable
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: ApproveGoldSetRequest

ResponseMeaningSchema
200Successful ResponseGoldSetView
422Validation ErrorHTTPValidationError

Requires: admin, operator, reviewer

ParameterInRequiredTypeNotes
item_idpathyesstring

Body: LabelGoldItemRequest

ResponseMeaningSchema
200Successful ResponseGoldItemView
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/filters/{filter_id}/modelsadmin, operator, reviewerList Models
GET/api/v1/filters/{filter_id}/thresholdadmin, operator, reviewerThe operating point awaiting approval, if any.
POST/api/v1/filters/{filter_id}/threshold/approveadmin, operatorApprove or reject a proposed operating point.
GET/api/v1/filters/{filter_id}/threshold/decisionadmin, operator, reviewerThe standing threshold decision for this filter’s current spec version.
POST/api/v1/filters/{filter_id}/trainadmin, operatorQueue a training job.
GET/api/v1/models/{model_id}admin, operator, reviewerGet Model

Requires: admin, operator, reviewer

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful Responsearray of ModelSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
atquerynonumber, nullable
ResponseMeaningSchema
200Successful ResponseThresholdProposal, nullable
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: ApproveThresholdRequest

ResponseMeaningSchema
200Successful ResponseThresholdProposal
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseThresholdDecisionView, nullable
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: TrainRequest

ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

Requires: admin, operator, reviewer

ParameterInRequiredTypeNotes
model_idpathyesstring
ResponseMeaningSchema
200Successful ResponseModelSummary
422Validation ErrorHTTPValidationError

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.

MethodPathRequiresSummary
GET/api/v1/filters/{filter_id}/driftadmin, operator, reviewerRecent drift readings.
GET/api/v1/filters/{filter_id}/eventsadmin, operator, reviewerThe filter’s events, newest first, optionally those given one decision.
GET/api/v1/filters/{filter_id}/filtered-eventsadmin, operator, reviewerEvery event this filter filtered, with score and model version.
GET/api/v1/filters/{filter_id}/metricsadmin, operator, reviewerWhat one filter did over the window, and what it missed.
GET/api/v1/filters/{filter_id}/runsadmin, operator, reviewerThe filter’s agent runs, newest first.
GET/api/v1/filters/{filter_id}/scores/histogramadmin, operator, reviewerThe Decide slot’s scores over the window, in 25 bins, with the threshold in force.
GET/api/v1/filters/{filter_id}/timelineadmin, operator, reviewerEverything that happened to this filter, newest first.

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful Responsearray of DriftObservation
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
decisionquerynoScoreDecision, nullable
rescued_byquerynostring, nullable
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
ResponseMeaningSchema
200Successful ResponsePage_EventView_
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
source_event_idquerynostring, nullable
startquerynostring, nullable
endquerynostring, nullable
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
causequerynoFilterCauseWhy 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.
ResponseMeaningSchema
200Successful ResponsePage_FilteredEventView_
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
window_daysquerynointegerDefault 7.
ResponseMeaningSchema
200Successful ResponseFilterCostMetrics
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 50.
legacy_event_idquerynobooleanDeprecated, for one release: true reports a group run’s event_id as "", as before, rather than null. Default False.
ResponseMeaningSchema
200Successful ResponsePage_AgentRunView_
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
window_daysquerynointegerDefault 7.
ResponseMeaningSchema
200Successful ResponseScoreHistogram
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
typesquerynoarray of TimelineKind, nullable
sincequerynostring, nullable
untilquerynostring, nullable
quiet_pollsquerynoQuietPollsDefault collapse.
cursorquerynostring, nullableOpaque cursor from a previous response’s next_cursor.
limitquerynointegerDefault 100.
event_idquerynostring, nullable
ResponseMeaningSchema
200Successful ResponseTimelinePage
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/activityadmin, operator, reviewerWhat every filter did, day by day, plus the trailing miss rate.
GET/api/v1/activity/filtersadmin, operator, reviewerOne series per named filter, for a page of list panels.
GET/api/v1/activity/filters/{filter_id}admin, operator, reviewerOne filter, day by day, for its own page.
GET/api/v1/activity/filters/{filter_id}/flowadmin, operator, reviewerOne filter’s window, as the paths its events took from the stream to the verdict.
GET/api/v1/activity/flowadmin, operator, reviewerEvery filter in view, as the paths its events took, and the paths summed.

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

ParameterInRequiredTypeNotes
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful ResponsePortfolioActivity
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
idsquerynoarray of string
window_daysquerynointegerDefault 7.
ResponseMeaningSchema
200Successful ResponseFilterActivityBatch
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
window_daysquerynointegerDefault 7.
ResponseMeaningSchema
200Successful ResponseFilterActivity
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
window_daysquerynointegerDefault 30.
basisquerynostring, nullableactual reads what happened, whatever the filter’s state: what became of what was sent, for Ingest and delivery.
ResponseMeaningSchema
200Successful ResponseFilterFlow
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful ResponseFleetFlow
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/alertsadmin, operator, reviewerEvery filter with a fault or something outstanding, worst first and uncapped.

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

ResponseMeaningSchema
200Successful ResponseAlertsSummary
MethodPathRequiresSummary
GET/api/v1/asksadmin, operator, reviewerEvery filter in view with a condition or a recommendation, worst first.
GET/api/v1/dashboardadmin, operator, reviewerAggregates across every filter over a trailing window.
GET/api/v1/decisionsadmin, operator, reviewerThe latest twenty state changes and approvals over the window, in the timeline’s words, each naming its filter.

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

ResponseMeaningSchema
200Successful ResponseFleetAsks

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

ParameterInRequiredTypeNotes
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful ResponseDashboardSummary
422Validation ErrorHTTPValidationError

The latest twenty state changes and approvals over the window, in the timeline’s words, each naming its filter.

Requires: admin, operator, reviewer

ParameterInRequiredTypeNotes
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful ResponseRecentDecisions
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/fleetadmin, operator, reviewerEvery filter in view, one row each, worst first.

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

ParameterInRequiredTypeNotes
window_daysquerynointegerDefault 30.
ResponseMeaningSchema
200Successful Responsearray of FleetRow
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/filters/{filter_id}/labelsadmin, operator, reviewerWhat training would count for this filter, by source, and its agent’s history.
POST/api/v1/filters/{filter_id}/labels/historyadmin, operatorTrain on the past verdicts of the agent this filter was built from.
DELETE/api/v1/filters/{filter_id}/labels/historyadmin, operatorRemove every historical label from this filter’s training set.

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseTrainingLabels
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
202Successful ResponseJobAccepted
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseTrainingLabels
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/admin/recipesadminEvery recipe in catalog order, with all three answers and who last decided.
POST/api/v1/admin/recipes/{recipe_id}/enabledadminTurn a recipe on or off for new filters.

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

ResponseMeaningSchema
200Successful ResponseRecipeSettingsOverview

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

ParameterInRequiredTypeNotes
recipe_idpathyesRecipeId

Body: SetRecipeEnabledRequest

ResponseMeaningSchema
200Successful ResponseRecipeSettingsOverview
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/filters/{filter_id}/verdict-checkadmin, operator, reviewerWhere the filter’s verdict check stands, and its queue.
PUT/api/v1/filters/{filter_id}/verdict-checkadmin, operatorSwitch the verdict check on or off for this filter.
POST/api/v1/filters/{filter_id}/verdict-check/items/{item_id}/answeradmin, operator, reviewerA person’s answer to one item.
POST/api/v1/filters/{filter_id}/verdict-check/resumeadmin, operatorResume 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

ParameterInRequiredTypeNotes
filter_idpathyesstring
itemsquerynobooleanDefault True.
ResponseMeaningSchema
200Successful ResponseVerdictCheckView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring

Body: VerdictCheckSwitch

ResponseMeaningSchema
200Successful ResponseVerdictCheckView
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
item_idpathyesstring

Body: VerdictCheckAnswerRequest

ResponseMeaningSchema
200Successful ResponseVerdictCheckItem
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
filter_idpathyesstring
ResponseMeaningSchema
200Successful ResponseVerdictCheckView
422Validation ErrorHTTPValidationError
MethodPathRequiresSummary
GET/api/v1/admin/webhooksadminEvery endpoint, its last delivery, and the rules they are all sent under.
POST/api/v1/admin/webhooksadminSave an endpoint, after the URL passes the egress check.
PATCH/api/v1/admin/webhooks/{endpoint_id}adminChange an endpoint. A new URL passes the egress check again; the secret rotates through rotate-secret instead.
DELETE/api/v1/admin/webhooks/{endpoint_id}adminDelete it. What it had queued is abandoned; its delivery log keeps its name.
GET/api/v1/admin/webhooks/{endpoint_id}/deliveriesadminThe last 100 deliveries, newest first, with attempts and errors.
POST/api/v1/admin/webhooks/{endpoint_id}/rotate-secretadminA new signing secret. For a day, every delivery carries a signature under both.
POST/api/v1/admin/webhooks/{endpoint_id}/testadminSend a webhook.test now, outside the queue, once, and say what came back.

Every endpoint, its last delivery, and the rules they are all sent under.

Requires: admin

ResponseMeaningSchema
200Successful ResponseWebhookOverview

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

ResponseMeaningSchema
201Successful ResponseWebhookEndpointSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
endpoint_idpathyesstring

Body: UpdateWebhookRequest

ResponseMeaningSchema
200Successful ResponseWebhookEndpointSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
endpoint_idpathyesstring
ResponseMeaningSchema
200Successful ResponseAck
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
endpoint_idpathyesstring
ResponseMeaningSchema
200Successful Responsearray of WebhookDeliverySummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
endpoint_idpathyesstring

Body: RotateWebhookSecretRequest

ResponseMeaningSchema
200Successful ResponseWebhookEndpointSummary
422Validation ErrorHTTPValidationError

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

ParameterInRequiredTypeNotes
endpoint_idpathyesstring
ResponseMeaningSchema
200Successful ResponseWebhookTestResult
422Validation ErrorHTTPValidationError