Skip to content

Channels API

A channel takes every report of the organization that its pre-filter matches and runs a workflow over it: triage steps, and transmit steps that deliver to a webhook or a local app, encrypted to a public key when the transmit step names one. The pre-filter is the channel’s only routing rule; the brief a report came through is one condition inside it. The workflow has numbered, immutable versions; the running version is the one dispatch executes. This page documents the channel and workflow version resources. Everything here is also in the Platform API reference.

Requests are JSON:API over HTTPS against https://api.disclosebot.io/v1 with a service account bearer token, as described in the API quickstart.

Terminal window
curl https://api.disclosebot.io/v1/channels \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx"
AttributeTypeMeaning
namestringUnique within the organization.
short_descriptionstring, nullableShown in the app.
enabledbooleanWhether dispatch routes reports to the channel.
enabled_atdate-time, nullableWhen it was last enabled.
prefilter_queryobject, nullableA custom pre-filter (see below). null when the channel references a saved search or has no pre-filter.
prefilter_saved_search_iduuid, nullableThe saved search the pre-filter reads. Its query applies as it is now: editing the search changes every channel that references it. Never set together with prefilter_query.
running_version_numberinteger, nullableThe workflow version dispatch runs.
steps_summaryarray of stringsFor example ["Pre-Filter", "Triage x5", "Transmit x1"].
created_at, updated_atdate-timeTimestamps.

Relationships: organization, workflow, and prefilter_saved_search. Pass include=workflow.versions or include=prefilter_saved_search to embed them.

A definition is a flat object: one entry per step, keyed by the step name, with needs naming the steps it runs after. Steps with no needs start together; a step with several needs waits for all of them. Names are lowercase letters, digits, and underscores, starting with a letter.

KindFields
triagetask (summary, scope, duplicate, validity, severity), engine (ai, or rule for scope and duplicate); severity takes scale (standard or custom with instructions, up to 2000 characters); duplicate takes lookback_days (1 to 365) and, with the ai engine, method (prompt or embeddings). One step per task and engine.
transmitintegration (`{ “type”: “Webhook"

Allowed only_when values: severity is critical, high, medium, or low; validity is valid or invalid; scope is in_scope or out_of_scope; duplicate is true or false.

The definition must draw as lanes: a step may not depend on two parallel lanes while both continue. A definition that cannot be drawn, has a cycle, or breaks any rule above is refused with 422 and one error per problem, each naming the step. At most 40 steps.

The standard workflow, four triage steps in parallel, severity after them, and a webhook delivery on critical findings:

Standard workflow definition
{
"steps": {
"summary": { "kind": "triage", "task": "summary", "engine": "ai" },
"scope": { "kind": "triage", "task": "scope", "engine": "rule" },
"duplicate": { "kind": "triage", "task": "duplicate", "engine": "ai", "method": "embeddings", "lookback_days": 90 },
"validity": { "kind": "triage", "task": "validity", "engine": "ai" },
"severity": { "kind": "triage", "task": "severity", "engine": "ai", "scale": "standard",
"needs": ["summary", "scope", "duplicate", "validity"] },
"jira": { "kind": "transmit", "integration": { "type": "Webhook", "id": "5d0c8a2e-…" },
"only_when": { "severity": ["critical"] }, "needs": ["severity"] }
}
}

A transmit step decides what the destination receives, on its own, without reading any other step:

The reportThe stepWhat the destination receives
OrdinaryNo public_key_idThe report as it stands.
Ordinarypublic_key_id setThe report’s description and the file names of its attachments, encrypted to that key (NaCl box, the format of libsodium and tweetnacl). The title travels readable.
Cloaked (encrypted in the reporter’s browser)cloaked is sendThe ciphertext the reporter’s browser produced, as it stands, encrypted to the brief’s Cloak Mode key. The step’s own public_key_id is not used: the platform cannot read a cloaked report, so it cannot re-encrypt one.
Cloakedcloaked is dropNothing of the report. The delivery request records dropped and carries no body.

An encrypted body is stored once per report and key, so two channels that encrypt to the same key deliver the same body. A step whose public_key_id names a key that no longer exists fails; the report is not delivered in the clear in its place.

Each channel gets its own transmit step run, so two channels sharing a destination both deliver. Triage steps are still shared: a report is triaged once, however many channels take it.

When a report crosses the dispatch gate, every enabled channel of the organization evaluates its pre-filter against it, as the pre-filter is at that moment. Each channel whose pre-filter matches processes the report; a channel with no pre-filter takes every report. The routing is recorded on the report’s event stream as Channel::Matched or Channel::NotMatched per channel.

The pre-filter is one of:

  • a reference to a saved search (prefilter_saved_search_id), whose query is read live;
  • a custom query of the channel’s own (prefilter_query);
  • nothing, when both are null.

Sending both answers 422 with the detail Prefilter query must be blank when the pre-filter references a saved search.

A query is two parts, the same shape a saved search stores:

{
"q": { "title_or_description_cont": "sql", "created_at_gteq": "2026-09-01" },
"results": { "severity": { "eq": "critical" } }
}

q holds ransack predicates over the report’s own fields (title_or_description, title, description, submitted_through_id (the brief, an array of ids with in or not_in), created_at, encrypted); results holds conditions on result keys. A flat object of ransack predicates is accepted and stored as q.

A channel keeps results conditions but ignores them at dispatch, because a result has no value before the workflow runs; the app shows them struck through. A brief condition (submitted_through_id_in or submitted_through_id_not_in) is how a channel chooses which briefs’ reports it takes; without one it takes reports from every brief.

A report carries a results attribute: the current value of each result key, one entry per key. A triage step, an integration, or the organization API writes a result; a later write replaces the earlier value.

{
"data": {
"id": "8e2298b2-f504-4249-9875-ef1a6d894e0e",
"type": "report",
"attributes": {
"title": "SQL injection in the login form",
"shortcode": "9d6147",
"inbox_state": "dispatched",
"processing_state": "open",
"processed_at": "2026-09-10T14:02:11.000Z",
"discarded_at": null,
"results": {
"severity": {
"value": "critical",
"reason": "Unauthenticated SQL injection in a login form",
"source": "triage",
"written_at": "2026-09-10T14:02:11.000Z"
},
"scope": {
"value": "in_scope",
"reason": "app.example.com is a listed asset",
"source": "triage",
"written_at": "2026-09-10T14:01:58.000Z"
}
},
"created_at": "2026-09-10T13:58:40.000Z",
"updated_at": "2026-09-10T14:02:11.000Z"
}
}
}

Each entry has:

FieldMeaning
valueThe current value. Triage keys use the values below; other writers choose their own.
reasonThe writer’s explanation, or null.
sourcetriage (a workflow step), integration (a connected app), or api (the organization API).
written_atWhen the value was written.

The triage steps of a workflow write these keys:

KeyValues
summaryfree text
severitycritical, high, medium, low
validityvalid, invalid
scopein_scope, out_of_scope
duplicatetrue, false

A report has no entries until a channel processes it; results is then {}. processed_at is the time the last triage step completed, or null. discarded_at is set when a brief with Self Destruct purges the report once every channel that took it has completed. A report whose processing failed in any channel (for example a delivery that failed) keeps its content until that processing is retried and completes, and a report that matches no channel is not purged. A discarded report keeps its shortcode, timestamps, and result keys; its title becomes Report <received date (UTC)> <shortcode> (for example Report 2026-09-08 9d6147), and its description, reporter email, attached files, and result values and reasons are removed. Deliveries already made are not recalled.

Result conditions in a saved search or a channel’s pre-filter read these same values.

GET /v1/channels

Filters: q[name_cont], q[short_description_cont], q[enabled_at_null] (true for disabled, false for enabled), q[created_at_gteq], and q[s] to sort (for example created_at desc). Pagination: page[number] and page[size] (at most 30).

Terminal window
curl "https://api.disclosebot.io/v1/channels?q[name_cont]=triage&q[enabled_at_null]=false&page[size]=20" \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx"
200
{
"data": [
{
"id": "3a4f…",
"type": "channel",
"attributes": {
"name": "Standard triage",
"short_description": "Triage and page",
"enabled": true,
"enabled_at": "2026-09-08T10:12:03.000Z",
"prefilter_query": { "q": { "title_or_description_cont": "sql", "submitted_through_id_in": ["9b1e…"] }, "results": {} },
"prefilter_saved_search_id": null,
"running_version_number": 2,
"steps_summary": ["Pre-Filter", "Triage x5", "Transmit x1"],
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-08T10:12:03.000Z"
},
"relationships": {
"organization": { "data": { "id": "6d4d…", "type": "organization" } },
"workflow": { "data": { "id": "c2d0…", "type": "workflow" } },
"prefilter_saved_search": { "data": null }
}
}
],
"links": { "self": "…", "first": "…", "last": "…" }
}

POST /v1/channels

Creates the channel with its pre-filter, records definition as workflow version 1, and enables the channel when enabled is true. Without definition the workflow starts empty.

Terminal window
curl -X POST https://api.disclosebot.io/v1/channels \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "channel",
"attributes": {
"name": "Standard triage",
"short_description": "Triage and page",
"enabled": true,
"prefilter_query": { "q": { "title_or_description_cont": "sql", "submitted_through_id_in": ["9b1e…"] }, "results": {} },
"definition": { "steps": { "summary": { "kind": "triage", "task": "summary", "engine": "ai" } } }
}
}
}'

To read a saved search live instead, send prefilter_saved_search_id and no prefilter_query; the Saved Searches API page has the example.

201 answers with the channel object above (running_version_number is 1). Add ?include=workflow.versions to embed the workflow and version 1.

422, invalid definition
{
"errors": [
{
"status": "422",
"title": "Unprocessable Entity",
"detail": "Step \"a\": kind must be one of triage, transmit",
"source": { "pointer": "/data/attributes/definition" }
}
]
}

A taken name answers 422 with the detail Name has already been taken.

GET /v1/channels/{id} answers 200 with the channel object, or 404 for a channel of another organization or an unknown id.

PATCH /v1/channels/{id}

Updates the fields and the pre-filter. The workflow is never changed here; commit a workflow version instead. To switch a channel from a saved search to a custom query, send the query with prefilter_saved_search_id: null; to switch it to a saved search, send the id with prefilter_query: null.

Terminal window
curl -X PATCH https://api.disclosebot.io/v1/channels/3a4f… \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "channel",
"attributes": {
"short_description": "Triage, then page the on-call",
"prefilter_saved_search_id": null,
"prefilter_query": { "q": { "title_cont": "sql" }, "results": {} }
}
}
}'

Enabling and disabling is the enabled attribute on the same PATCH:

Disable
curl -X PATCH https://api.disclosebot.io/v1/channels/3a4f… \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "data": { "type": "channel", "attributes": { "enabled": false } } }'
Enable
curl -X PATCH https://api.disclosebot.io/v1/channels/3a4f… \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "data": { "type": "channel", "attributes": { "enabled": true } } }'

Both answer 200 with the channel object; enabled and enabled_at reflect the new state. Disabling lets reports already in flight finish on the running version; nothing new is dispatched.

DELETE /v1/channels/{id} answers 204 and removes the channel with its workflow and every version. Reports already processed keep their history.

GET /v1/channels/{channel_id}/workflow_versions

Oldest first. Filters: q[number_gteq], q[s] (for example number desc). Pagination as above.

200
{
"data": [
{
"id": "e7a1…",
"type": "workflow_version",
"attributes": {
"number": 1,
"definition": { "steps": { "summary": { "kind": "triage", "task": "summary", "engine": "ai" } } },
"definition_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"committed_at": "2026-09-01T09:00:00.000Z",
"committed_by_id": null,
"summary": "Initial version",
"created_at": "2026-09-01T09:00:00.000Z"
},
"relationships": { "workflow": { "data": { "id": "c2d0…", "type": "workflow" } } }
}
]
}

GET /v1/channels/{channel_id}/workflow_versions/{id} answers 200 with one version, or 404.

POST /v1/channels/{channel_id}/workflow_versions

Records the definition as the next version and makes it the running version. The response summary lists what changed against the previous running version. Reports already in flight finish on the version they started on.

Terminal window
curl -X POST https://api.disclosebot.io/v1/channels/3a4f…/workflow_versions \
-H "Authorization: Bearer disclosebot_svc_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "workflow_version",
"attributes": { "definition": { "steps": { "…": "…" } } }
}
}'

201 answers with the new version (number is the previous number plus one).

409, nothing changed
{
"errors": [
{
"status": "409",
"title": "Conflict",
"detail": "No changes to commit",
"source": { "pointer": "/data/attributes/definition" }
}
]
}

A definition that fails the checks answers 422 in the same shape as create, one error per problem.

Errors are a JSON:API errors array of objects with status, title, detail, and, for definition and attribute problems, source.pointer.

StatusWhen
401Missing or invalid bearer token.
403Channels are not enabled for the account (Feature not available).
404Unknown id, or a channel or version of another organization.
409Committing a definition equal to the running version.
422A definition that breaks a rule, a taken name, or an invalid attribute.
  • page[number] and page[size] (default and maximum 30); responses carry meta.pagination (current, records = the total, and first, prev, next, last when they exist) and links for the same pages. See Pagination.
  • q[...] filters use ransack predicates on the attributes listed per endpoint; q[s] sorts.
  • include embeds related records: workflow, workflow.versions, and prefilter_saved_search on channels.