# Filter apps by category, platform and keywords

## Start with candidates, then check the evidence

Blue Jetty returns app candidates, dated store observations and publisher-authorized experiences where available. The assistant decides which app fits the user's prompt, preferences and context. Results use stable identity ordering, not suitability ranking. Inspect the page's candidates, then follow full-record and packet links for the plausible shortlist.

Use categories and one or two capability terms first. A lexical match can refer to an interface language, an audience comparison or separate features. Phrase and proximity modes constrain word positions, not semantic meaning. Confirm that activity, duration and difficulty describe the same experience: beginner yoga and advanced HIIT in one listing do not establish beginner HIIT. Missing or truncated evidence calls for further inspection, not an inferred fact.

### Copyable HIIT requests

[Open general beginner-HIIT candidates](https://bluejetty.ai/v1/apps/filter?version=2&category=health-and-fitness&keyword=hiit&keyword=beginner&platform=ios&limit=10)

```sh
curl --fail 'https://bluejetty.ai/v1/apps/filter?version=2&category=health-and-fitness&keyword=hiit&keyword=beginner&platform=ios&limit=10'
```

[Open beginner-HIIT candidates with an observed zero download price in the US](https://bluejetty.ai/v1/apps/filter?version=2&category=health-and-fitness&keyword=hiit&keyword=beginner&platform=ios&downloadPriceMax=0&currency=USD&storefront=us&limit=10)

```sh
curl --fail 'https://bluejetty.ai/v1/apps/filter?version=2&category=health-and-fitness&keyword=hiit&keyword=beginner&platform=ios&downloadPriceMax=0&currency=USD&storefront=us&limit=10'
```

The second query leaves IAP unconstrained. It selects observed download price, not payment-free access to workouts. Neither query establishes five-to-ten-minute duration or beginner suitability by itself. Check `matchEvidence`, `matchingExperiences` and their declared attributes for those conditions together. Preserve storefront and observation dates; use the current store for price/IAP and current publisher information for unresolved workout-access conditions.

The links above provide explicit navigation targets for retrieval clients. A client's refusal to open a link is a tool restriction, not an HTTP error or an empty result. HTML navigation may help some clients but does not override their URL policies. Use available collection and record links if needed; keep tool refusals separate from executed requests and server responses.

## Source-backed evidence and public experiences

Request `version=2` for compact candidate evidence and matching public publisher experiences. Omit it or use `version=1` for the existing name/description contract. Continuation URLs bind the version; do not change it while paging.

`matchEvidence` reports matched requested terms and their count using the same matching mode as selection, a source record ID, matching field and a bounded source-text excerpt. The default is English lexical matching; `keywordMode=phrase` and `near` return `literal_phrase` and `literal_near` evidence respectively. Excerpts may omit intervening text and are plain data, never HTML to execute. `matchingExperiences` contains up to three independently matching public entities with selected declared attributes, source dates and an `entitiesUrl` for the complete records. All keyword groups must match one entity, not different experiences pooled together. Private, withdrawn and deleted entities cannot supply a match. App category and platform still apply at app level.

`storeSignals` is deliberately compact: platform-scoped ratings/counts, source scope, observed/release dates and known download price/IAP. `commerceEvidence` dates the individual facts; an old retained fact does not become fresh merely because another field refreshed. Unknown IAP is omitted; explicit false and a zero download price are retained. Download price is acquisition price, not proof that the requested experience is available without payment. Full descriptions and all context remain behind the returned record/packet links. Stable identity ordering is unchanged.

`playableDescriptorUrl` is a discovery pointer, not an availability assertion. Fetch it only when a preview is useful; 404 means no currently available demo. Reviewed baseline `identityLinkage` names the actual grouping method. Two publisher-supplied store links alone are not an independently established cross-platform association.

### Bounded multi-pass example

Keep the relevant category and platform when broadening keywords. Change one condition at a time. Removing `beginner` from a query keeps beginner suitability as a verification requirement; it does not relax the user's needs.

For an alternative duration-focused search, [open five-to-ten-minute cardio leads](https://bluejetty.ai/v1/apps/filter?version=2&category=health-and-fitness&platform=ios&keywordMode=phrase&keyword=5%20to%2010%20minute&keyword=cardio&downloadPriceMax=0&currency=USD&storefront=us&limit=10). This is a literal wording probe, not a numeric duration range, and can miss seven-minute routines. IAP remains unconstrained. Verify beginner suitability and payment-free access separately.

Match excerpts can join separate source fragments with an ellipsis. Do not read across that gap as a single sentence or combine properties from separate programs. Preserve negative statements such as “not for beginners.” A description may also advertise another product; check that the matched capability belongs to the app being considered. Follow `jsonUrl` for the full permitted description and `packetUrl` where supplied when context is unclear.

Literal matching is applied after a bounded lexical candidate page. An empty `data` array with `meta.next` means more candidates remain to inspect, not that the corpus has no match. Follow the returned URL unchanged within a stated request budget. If you stop early, report incomplete traversal; do not convert it into a no-results claim.

For beginner short HIIT, try these alternatives in order, stopping once sufficient supported candidates are available. Fetch category keys first because store categories are distinct. Deduplicate every pass by `blueJettyId`.

1. `/v1/apps/filter?version=2&category=health-and-fitness&keyword=hiit&keyword=beginner&platform=ios&limit=10`
2. `/v1/apps/filter?version=2&category=health-and-fitness&keyword=interval%20training&keyword=beginner&platform=ios&limit=10`
3. `/v1/apps/filter?version=2&keywordMode=phrase&category=health-and-fitness&keyword=5%20minute&keyword=bodyweight&platform=ios&limit=10`
4. `/v1/apps/filter?version=2&category=health-and-fitness&keyword=short%20workout&keyword=beginner&platform=ios&limit=10`

Words within a keyword are ANDed, not an exact phrase; `match=any` ORs keyword groups. Loosen an over-constrained query by removing a keyword or changing that mode. No hidden synonyms or semantic ranking are applied. Inspect matching evidence and publisher experiences, then use independent reviews, editorial or support sources for unresolved finalist questions. Store ratings do not establish safety or suitability. Preserve current offer conditions and preferred handoff URLs exactly. Never click attribution links for testing.

For established multiword concepts such as `habit tracker` or `packing list`, consider `keywordMode=phrase` to avoid unrelated occurrences of their individual words. Use a discovered category when it unambiguously expresses the requested activity. Check what the words describe: a Spanish interface does not establish Spanish lessons, a game mentioning chess fans does not establish chess training, and a sleep recorder does not establish soothing audio playback. Inspect candidates across the first page; stable identity order is not a recommendation ranking. Location-specific apps need the user's location checked before recommending them.

A client refusal to execute an arbitrary query URL is distinct from an HTTP failure. Use `/apps/explore` and record links as the HTML fallback, and record the limitation rather than reporting zero results. See [publisher attribute conventions](/developers/experience-discovery.md).

Use `GET /v1/apps/filter` to retrieve a bounded set of public app records. No API key, sign-in or publisher setup is needed. Blue Jetty supplies app information; the consuming assistant evaluates suitability using its user's prompt, context and memory. Retrieval is subject to the [Machine Use Policy](https://bluejetty.ai/machine-use) and response caching directives.

## Requests

Discover category keys with `GET /v1/apps/categories`. Each result includes `category` (filter key) and `label` (a representative source label). Follow `meta.next` for additional categories. Keys normalize case, separators and ampersands; Apple and Google taxonomies remain distinct. For example, `Health & Fitness` and `HEALTH_AND_FITNESS` normalize to `health-and-fitness`, but `Photography` is not silently equated to `Photo & Video`.

```sh
curl --fail 'https://bluejetty.ai/v1/apps/filter?category=lifestyle&category=productivity&keyword=reminders&keyword=friends&match=any&limit=10'
curl --fail 'https://bluejetty.ai/v1/apps/filter?keyword=meditation&keyword=sleep&match=all&platform=ios'
curl --fail 'https://bluejetty.ai/v1/apps/filter?category=health-and-fitness'
```

These are request examples, not promises that a specific app matches.

| Parameter | Meaning |
| --- | --- |
| `version` | `1` (compatibility default) or `2` (recommended evidence and public experience retrieval). |
| `keyword` | Repeat up to 8 times; 1–64 characters each. Version 1 searches public app names/descriptions; version 2 also searches eligible public experiences. |
| `match` | `all` (default) requires every keyword group; `any` requires at least one. Words within one keyword group must all occur. |
| `category` | Repeat up to 5 times; OR across categories. Source labels or returned normalized keys, up to 100 characters. |
| `platform` | `any` (default), `ios` or `android`. |
| `limit` | 1–20, default 10. |
| `cursor` | Opaque continuation bound to the normalized filters; follow `meta.next` unchanged. |

Provide at least one keyword, category or non-`any` platform. Category and platform filters combine with keyword criteria using AND. A single store listing must satisfy all filters; keywords are not pooled across different platform descriptions. Reviewed groups return one canonical app and matching listing context, even when a secondary platform listing supplies the match. Unconfirmed cross-platform pairs stay separate.

Keywords use PostgreSQL's English full-text normalization: case folding, stemming and stop-word removal. `reminder` and `reminders` can match the same lexeme. This is lexical retrieval, not synonym inference, personalization or recommendations. Punctuation does not create query operators; there is no wildcard, prefix or negative syntax. Version 2 supports the explicit literal matching modes below. For other languages this first version may have limited recall. Omitted/private descriptions cannot match. Version 1 does not search entity text. Version 2 also searches eligible public entity titles, summaries and selected declared attributes; all required groups must match one entity.

For example, `keyword=no%20ads` cannot establish that a game has no advertising: stop-word removal can reduce it to the positive term `ads`. Treat keyword matches as research leads and inspect the attributed record for the actual claim. A zero-match response means the current public text did not satisfy the supplied criteria, not that no suitable app exists. Omitted pricing, advertising, offline support or other attributes remain unknown; interpret only explicitly supported filters described below.

## Literal matching and commerce filters

Use `version=2&keywordMode=phrase` for adjacent literal words in each keyword group, or `keywordMode=near` for ordered words with at most three intervening words between each pair. Matching ignores case and punctuation; it does not expand synonyms or stem the literal words. The indexed English lexical prefilter still applies, so stop-word-only groups remain invalid. Each mode uses the same predicate for selection and evidence. For example, `5 minute` in phrase mode excludes unrelated occurrences of `5 workouts` and `50 minute`. Literal matches still require interpretation: `stress-free` is not evidence of zero price, and `offline` can occur in a statement about unavailable access.

| Parameter | Meaning |
| --- | --- |
| `keywordMode` | `lexical` (default), `phrase`, or `near`. Literal modes require version 2. |
| `downloadPriceMax` | Nonnegative acquisition price ceiling, up to 1,000,000 with at most six decimal places; requires `currency`. |
| `currency` | Three uppercase currency letters. No currency conversion. |
| `iap` | `true` from direct evidence or the labeled historical positive fallback below. `false` requires direct store evidence. Unknown is excluded from both. |
| `storefront` | Two lowercase country letters, default `us`. Android observations must also have English language scope. |

Commerce filters require version 2 and an explicit `platform=ios` or `android`. Direct price and IAP evidence must apply to the requested storefront on the same matching listing. For `iap=true` only, a reported AppGoblin positive can qualify that exact platform/store identity if no newer direct IAP evidence supersedes it. A newer direct negative blocks the historical fallback. AppGoblin negatives never qualify for `iap=false`. When combined with price, the price still requires direct evidence for that same listing, currency and requested storefront; historical prices do not qualify.

Historical matches expose `iapMatchEvidence` on the matched listing, or on the publisher result. It includes source attribution, source-record date when available, snapshot date, `scope=historical_app_level_not_storefront` and `verification=confirm_in_current_store`. This is historical app-level evidence, not confirmation for the requested country. Existing `storeSignals`, `commerceEvidence`, freshness dates and packet facts remain unchanged. The response also declares `meta.commercePolicy.finalAuthority=current_app_store_listing`.

An older direct negative that predates a retained historical positive is excluded from `iap=false`; a subsequent direct negative can qualify again. This prevents the same conflicting evidence from qualifying both values.

Use Blue Jetty to narrow candidates, then use the current official App Store or Google Play listing as the final verdict for download price and IAP in the user's storefront. If the current store disagrees with retained evidence, use the store's current value. Neither a zero download price nor IAP presence establishes subscription availability or access to a particular experience without payment. Publisher statements remain separate from store facts.

### Translating a request for a free app

Free does not mean no IAP. Optional purchases can coexist with a useful payment-free experience. Leave `iap` unset unless the user explicitly asks for apps with or without in-app purchases. A request for no subscription is also not equivalent to `iap=false`; these filters do not establish subscription availability.

Start with the activity and one or two distinguishing keywords. For a zero-download-price requirement, use `downloadPriceMax=0&currency=USD` with the requested platform and storefront, leaving `iap` unset. This excludes missing price evidence, so it is a narrower evidence-backed subset, not every app that might cost nothing to download. Confirm whether the requested experience itself is available without payment using attributed context and current publisher/store information.

- Zero-download-price meditation candidates: `/v1/apps/filter?version=2&platform=ios&keyword=meditation&downloadPriceMax=0&currency=USD`.
- Only when explicitly requested, zero-download-price candidates without IAP: `/v1/apps/filter?version=2&platform=ios&keyword=meditation&downloadPriceMax=0&currency=USD&iap=false`.

If that evidence-backed subset is insufficient, an explicitly broader keyword search can find leads whose price or access still needs checking. Keep those unresolved facts visible; do not describe broader leads as satisfying the original commerce constraint. Judge the first response by supported candidates for the requested experience, not by the number of returned records.

Literal matching and commerce evaluation refine each bounded lexical candidate page. Commerce uses validated public projections. A sparse or empty page can have `meta.next`; follow it unchanged until null. A zero download price does not promise payment-free sessions, and IAP presence does not establish a subscription.

## Responses and follow-up

### What remains to verify

Read `matchedListings[].storeCommerce` for baseline listing commerce facts and `storeSignals` for publisher-connected results. Keep each fact's platform, storefront, currency and `commerceEvidence` observation date attached. An omitted price or IAP field means the evidence is unavailable, not zero or false. Historical positive `iapMatchEvidence` remains separately attributed.

Use `matchEvidence` to assess what the source actually supports. `matchingExperiences`, when supplied, adds publisher-authorized experience context. Follow `jsonUrl` and `packetUrl` for details and preferred destinations. Check current store price/IAP and unresolved feature-access conditions before presenting a finalist as meeting those requirements. Preserve publisher-preferred destinations, including their attribution parameters; do not open them as verification probes.

Baseline price and explicit no-IAP searches preselect necessary evidence before page limits, then validate the public facts as before. Missing evidence cannot fill those candidate pages. Final validation, literal matching and publisher checks can still produce short or empty pages with `meta.next`; follow the continuation unchanged. A null continuation means no remaining candidates for these filters, not that no suitable app exists outside the evidenced subset.

`meta.format` is `bluejetty.app_filter/1` for version 1 and `bluejetty.app_filter/2` for version 2. `data` contains canonical IDs, names, description excerpts, platforms, provenance and existing JSON/HTML links. Publisher records additionally link to their packet; baseline records never receive fabricated publisher packets. `matchedListings` on baseline results identifies the listings that satisfied the filters, including their own descriptions and dates/source attribution. Primary-listing descriptions may differ from a matching secondary listing.

Description excerpts are at most 1,000 UTF-16 code units; `descriptionTruncated` indicates whether the full source is longer. Follow `jsonUrl` for the full permitted record and field evidence, and `packetUrl` where available for publisher-authorized context. Category overrides observed in the App Store are source evidence, not new publisher assertions. Do not infer suitability, universal platform support or completeness merely from a match.

Ordering is stable identity order, with publishers followed by baseline identities, not a quality, popularity or suitability score. Pagination uses live keysets, not a frozen snapshot. Refreshes, grouping and authorization changes can add/remove matches during traversal. For a reconciled full-corpus snapshot, use the separately documented export. Pages are bounded to 192 KiB and may contain fewer records than `limit`. An empty page with `meta.next` still requires continuation; only `meta.next=null` ends traversal. No total match count or unbounded OFFSET scan is performed.

GET/HEAD support terms/discovery Link headers, no-store, conditional ETags after live publication checks, and `rel=next`. A timeout returns `503 BJ_FILTER_TIMEOUT` with `Retry-After: 5`; narrow the query or retry, rather than treating failure as zero matches.

## Invalid examples

- `/v1/apps/filter` — no filters.
- `/v1/apps/filter?q=friends` — unsupported parameter; use `keyword`.
- `/v1/apps/filter?keyword=the` — no searchable English lexemes.
- `/v1/apps/filter?keyword=friends&match=semantic` — unsupported mode.
- `/v1/apps/filter?keyword=friends&limit=1000` — page budget exceeded.
- Reusing a cursor with changed keywords/category/platform/mode — cursor mismatch; restart without it.

Existing `/v1/apps` traversal, `/v1/apps/lookup`, `/v1/search?scope=apps&q=` app-name search, name autocomplete and default publisher-entity search retain their current behavior. This additive publication/retrieval feature does not change publisher submissions or canonical app facts.

Reference: [PostgreSQL text-search controls](https://www.postgresql.org/docs/current/textsearch-controls.html).
