8 routes
/filters
8 routes.
Saved queries over to-dos. criteria is a JSON string using tag NAMES. It is parsed on create and on patch, and a document the grammar does not accept is validation_failed; only a STORED filter from a newer or older grammar can be inert (it matches nothing).
- GET /v1/filters
- POST /v1/filters
- GET /v1/filters/{id}
- GET /v1/filters/{id}/todos
- PATCH /v1/filters/{id}
- POST /v1/filters/{id}/trash
- POST /v1/filters/{id}/restore
- DELETE /v1/filters/{id}
GET /v1/filters
List filters
The live filters. A filter in the trash is left out unless include_trashed=true (then it carries its trashedAt).
Scopes: filters:read
Query
| parameter | type | notes | |
|---|---|---|---|
limit |
integer | optional | min 1, max 200 |
cursor |
string | optional | max 4096 chars |
updated_since |
string (RFC 3339) | optional | max 24 chars. An RFC 3339 instant, e.g. 2026-09-16T08:30:00.000Z. |
include_trashed |
true, false |
optional |
Response
- 200 FilterList. One page of saved filters.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * account_too_large * validation_failed * rate_limited * internal
POST /v1/filters
Create a filter (criteria uses tag names)
criteria is PARSED. A document that does not match the grammar (a v1 one, an unknown kind or operator, a missing match, an empty set, a fractional within) is validation_failed at /criteria with the grammar in the message, instead of being stored as a filter that silently matches nothing. What is stored is the engine’s own canonical serialisation, so two filters that mean the same thing are the same bytes.
Scopes: filters:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Request body
| field | type | notes | |
|---|---|---|---|
name |
string | required | max 256 chars, min 1 |
criteria |
string | required | max 10000 chars, min 1 |
symbol |
string | optional | or null, max 64 chars |
id |
string (UPPERCASE UUID) | optional | max 64 chars. An uppercase UUID, all 36 characters, hex digits in capitals: 8E4F2A1B-7C3D-4E5F-9A0B-1C2D3E4F5A6B. Supplying one is how a create is made idempotent; a repeat is 409 duplicate_id. Lower case is refused, and so is a short id. |
Response
- 201 Filter. The filter as stored.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * conflict * duplicate_id * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
GET /v1/filters/{id}
One filter
Scopes: filters:read
Response
- 200 Filter. The filter.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal
GET /v1/filters/{id}/todos
Evaluate a filter with the engine
Evaluated by the same engine the apps run. ⚠ An unparseable or unknown criterion makes a filter INERT: it matches nothing rather than erroring, so an empty page can mean "no matches" or "this filter is broken"; read the filter’s criteria to tell.
Scopes: filters:read, todos:read
Query
| parameter | type | notes | |
|---|---|---|---|
limit |
integer | optional | min 1, max 200 |
cursor |
string | optional | max 4096 chars |
Response
- 200 TodoList. One page of matching to-dos.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * validation_failed * rate_limited * internal
PATCH /v1/filters/{id}
Change a filter
Sending criteria REPLACES the whole set and is parsed exactly as on POST. after (a filter id, or null for first) or before reorders the filter list.
Scopes: filters:write
Request body
| field | type | notes | |
|---|---|---|---|
name |
string | optional | max 256 chars, min 1 |
criteria |
string | optional | max 10000 chars, min 1 |
symbol |
string | optional | or null, which clears it, max 64 chars |
after |
string | optional | or null, which clears it, max 64 chars. Reorder the filter list: right after this filter, or null for first. |
before |
string | optional | max 64 chars. Reorder the filter list: right before this filter. |
Response
- 200 Filter. The filter after the change.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
POST /v1/filters/{id}/trash
Move a filter to the trash
One write: the filter is stamped trashedAt. It matches nothing and GET /filters leaves it out until /restore; the trash deletes it for good after 30 days (or DELETE /trash). Its to-dos are never touched.
Scopes: filters:write
Response
- 200 Filter. The filter, now stamped
trashedAt.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/filters/{id}/restore
Restore a filter from the trash
Scopes: filters:write
Response
- 200 Filter. The filter, with
trashedAtcleared.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
DELETE /v1/filters/{id}
Delete a filter
Irreversible, live or in the trash (the apps’ delete moves a filter to the trash: POST /filters/{id}/trash). The to-dos it matched are untouched.
Scopes: filters:write
Response
- 204 No body. Deleted.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal