Skip to main content

Filters

  • Processor — looks at each item in your feeds and records answers ("outputs"); for example Event labels records age_restricted.confidence: 0.92. A processor never includes or excludes anything. Built-in processors are ours; you can create your own. A processor belongs to your organization, not to a feed.
  • Filter — a rule on one feed: include or exclude items whose processor outputs meet a condition. An item is in the feed when no exclude filter matches and at least one include filter does. A feed runs exactly the processors its filters read.

A feed has three kinds of filter:

  • The safety floor. Ours, on every feed. It excludes unsafe items, cannot be switched off and is not listed among a feed's filters.
  • Opt-in filters. Ours, added by you. You can add one or remove it, but not edit it: its condition always follows our latest definition. See Opt-in filters.
  • Your own filters. Your condition, fully under your control.

A new event feed starts with one filter, the completeness opt-in, which leaves out events with too little detail to list. Remove it if your feed should not have it. A new news feed starts with none.

Includes and excludes​

An item is in the feed when no exclude filter matches and at least one include filter does. Excludes always win. Includes are alternatives: each include you add lets more items in.

This matters when you mix our opt-ins with your own filters. Your own include widens the feed, it does not narrow it. Say your feed has the "Community events" set, whose "In the area" include lets in events in your area, and you add this include:

{
"name": "Family events",
"action": "include",
"condition": {
"processor": "event_labels",
"output": "family_friendly.confidence",
"op": ">=",
"value": 0.8
}
}

The feed now carries events in the area and also family events from anywhere. To carry only family events in the area, write one include that requires both (and remove "In the area"), with your area match processor's id in place of AREA_MATCH_ID:

{
"name": "Family events in the area",
"action": "include",
"condition": {
"all": [
{
"processor": "AREA_MATCH_ID",
"output": "match",
"op": "=",
"value": "match"
},
{
"processor": "event_labels",
"output": "family_friendly.confidence",
"op": ">=",
"value": 0.8
}
]
}
}

To leave something out, write an exclude: it removes what it matches whatever the includes say.

An enabled feed must keep at least one include filter. Removing its last include, or turning it into an exclude, is refused with 422 LAST_INCLUDE_FILTER; add another include first, or disable the feed. Enabling a feed (PATCH /feeds/events/{id} or /feeds/news/{id} with enabled: true) is refused with 422 VALIDATION_FAILED while it has no include. A new feed starts disabled: add an include, then enable it. An event feed created with enabled: true is refused the same way, since a new feed has no include yet.

Conditions​

A condition is a leaf, or all / any of conditions:

{
"any": [
{
"processor": "event_labels",
"output": "age_restricted.confidence",
"op": ">=",
"value": 0.6
},
{
"processor": "event_category",
"output": "primary",
"op": "in",
"value": ["nightlife"]
}
]
}

A leaf is true when the processor recorded an output that meets the comparison:

FieldDescription
processorA built-in's key (e.g. event_labels), or the id of one of your processors. Built-ins always come back as their key.
outputOne of the processor's outputs (GET /processors/{id}), e.g. promotional.confidence, score or answer
op=, !=, >=, >, <=, < or in. number outputs take every operator; enum and boolean outputs take =, != and in
valueA number in the output's range, one of an enum output's values, or true / false. With in, a list of them (1 to 100)

all is true when every condition in it is true, any when at least one is. A condition nests at most 3 levels of all / any deep and has at most 50 leaves; a deeper or larger one is refused with 400 INVALID_BODY, as is one that is not a tree of leaves and groups (a leaf missing a field, an empty all or any). The problems below are about what the leaves name, never the condition's shape.

A filter can read the built-ins and your organization's processors that run on the feed's item type and are not archived. Every leaf is checked when you write it. A leaf that does not check out is refused with 422 INVALID_CONDITION, listing each problem with its path:

{
"success": false,
"error": {
"code": "INVALID_CONDITION",
"message": "The condition cannot be saved: condition.any[0].output: …",
"details": {
"problems": [
{
"path": "condition.any[0].output",
"code": "unknown_output",
"message": "…"
}
]
}
}
}
Problem codeMeaning
unknown_processorNo such processor, or another organization's
archived_processorThe processor is archived
wrong_item_typeThe processor does not run on the feed's item type
not_readableThe processor has no output a filter can read
unknown_outputThe processor has no such output
bad_operatorThe operator does not suit the output's type
bad_valueThe value is out of the output's range, or not one of its values
unknown_paramOpt-ins only: a parameter in with the opt-in does not take
missing_paramOpt-ins only: a parameter the opt-in requires is missing, or areaRelevance is given without areaMatch
wrong_templateOpt-ins only: the parameter's processor is not of the template the parameter takes

The same codes are the details.problems of an opt-in's refusals, at paths with.<parameter>: 422 MISSING_OPT_IN_PARAM for missing_param, and 422 INVALID_OPT_IN_PARAM for the others. A processor's own definition is checked differently: a definition its template refuses is a 422 VALIDATION_FAILED on the processor route (see Processors), not INVALID_CONDITION.

Changes apply going forward​

Adding, changing or removing a filter applies to items evaluated from then on: new items, and items already in the feed when they next change. The feed is not re-evaluated when you save. An item that stops passing is removed from the feed when it is next evaluated, and a removal webhook is sent for it.

Who can see a feed's filters​

Filters belong to the feed's owner. Another organization subscribed to the feed gets 403 PERMISSION_DENIED on every filter route; any other organization gets 404.

API reference​

The routes are the same for both feed types: {type} is events or news.

RouteDoes
GET /feeds/{type}/{id}/filtersThe feed's own and opt-in filters, oldest first (never the safety floor)
GET /feeds/{type}/{id}/filters/{filterId}One filter
POST /feeds/{type}/{id}/filtersAdd your own filter, or one of our opt-in filters
PATCH /feeds/{type}/{id}/filters/{filterId}Change one of your own filters
DELETE /feeds/{type}/{id}/filters/{filterId}Remove a filter, own or opt-in
POST /feeds/{type}/{id}/filter-sets/{setKey}Add a set of opt-in filters. See Opt-in filters

The filter​

FieldDescription
idStable identifier: the filterId of the routes above
nameThe filter's name. Free text, not unique
actioninclude or exclude
typeown (yours, editable) or opt_in (ours, removable only)
optInOpt-ins only: key and with, the processors it was added with
conditionOwn filters: the condition as written. Opt-ins: the condition generated from our current definition, read-only. Built-ins named by their key
createdAt, updatedAtISO 8601

Add a filter​

POST /feeds/{type}/{id}/filters

Your own filter, with name, action and condition:

{
"name": "No family-unfriendly events",
"action": "exclude",
"condition": {
"any": [
{
"processor": "event_labels",
"output": "age_restricted.confidence",
"op": ">=",
"value": 0.6
},
{
"processor": "event_category",
"output": "primary",
"op": "in",
"value": ["nightlife"]
}
]
}
}

On a news feed, for example:

{
"name": "Strong news stories",
"action": "include",
"condition": {
"processor": "news_content_type",
"output": "editorial_news.significance",
"op": ">=",
"value": 7
}
}

Or one of our opt-in filters, with optIn and the processors it takes in with (see Opt-in filters):

{
"optIn": "in_area",
"with": {
"areaMatch": "5c0e8f9a-3b1d-4f6e-9a2c-7d4b1e0f8a63",
"areaRelevance": "0a7d2c4e-9f1b-4e3a-8c6d-1b2e3f4a5c6d"
}
}

Response 200 OK, the filter:

{
"success": true,
"data": {
"id": "7f3c2b1a-6e5d-4c4b-9a3f-2e1d0c9b8a71",
"name": "In the area",
"action": "include",
"type": "opt_in",
"optIn": {
"key": "in_area",
"with": {
"areaMatch": "5c0e8f9a-3b1d-4f6e-9a2c-7d4b1e0f8a63",
"areaRelevance": "0a7d2c4e-9f1b-4e3a-8c6d-1b2e3f4a5c6d"
}
},
"condition": {
"any": [
{
"processor": "5c0e8f9a-3b1d-4f6e-9a2c-7d4b1e0f8a63",
"output": "match",
"op": "=",
"value": "match"
},
{
"processor": "0a7d2c4e-9f1b-4e3a-8c6d-1b2e3f4a5c6d",
"output": "answer",
"op": "=",
"value": "yes"
}
]
},
"createdAt": "2026-10-28T14:05:00.000Z",
"updatedAt": "2026-10-28T14:05:00.000Z"
}
}

An own filter comes back the same way, with type: "own", no optIn, and its condition as you wrote it.

Change a filter​

PATCH /feeds/{type}/{id}/filters/{filterId}

Send any of name, action and condition; the others stay as they are.

{
"condition": {
"processor": "event_labels",
"output": "age_restricted.confidence",
"op": ">=",
"value": 0.8
}
}

Only your own filters can be changed. An opt-in filter is refused with 403 OPT_IN_NOT_EDITABLE: remove it and add your own filter instead.

Remove a filter​

DELETE /feeds/{type}/{id}/filters/{filterId}

Removes an own or opt-in filter. Returns { "success": true }.

Errors​

StatusCodeWhen
400INVALID_BODYThe body is malformed, a condition is deeper than 3 levels or has more than 50 leaves, or optIn is not an opt-in of this feed type
403PERMISSION_DENIEDYour organization subscribes to the feed but does not own it
403OPT_IN_NOT_EDITABLEA PATCH of an opt-in filter
404NOT_FOUNDNo such feed or filter, or a filter of another feed
409FILTER_ALREADY_PRESENTThe opt-in decides the same as a filter the feed already has. details.equalTo is that filter's id
422INVALID_CONDITIONA leaf does not check out. details.problems[{ path, code, message }]
422MISSING_OPT_IN_PARAMA processor the opt-in requires is missing. details.problems, paths with.<parameter>
422INVALID_OPT_IN_PARAMA processor the opt-in cannot take. details.problems, paths with.<parameter>
422LAST_INCLUDE_FILTERRemoving the last include of an enabled feed, or turning it into an exclude