# Market overview with facets

> Counts by category, weekday, time, price, age and organization over a whole filtered set.

Source: https://api.tailend.dev/docs/market-overview-with-facets

This page is for anyone showing "the market near here": how many classes and
events, on which days, at what times and prices, for which ages, run by whom.
You can get those numbers without downloading the listings. Add `facets=` to a
list request and the response carries counts over **every listing that matches
your filters**, not just the page you asked for.

It also covers the other half of the same question: when a count looks smaller
than you expected, the response tells you which filters produced it.

## Two things to know first

Three filters narrow a list even when you do not send them:

| Filter | Default | Applies to |
| --- | --- | --- |
| `from` / `to` | `from` is today in the metro; on events `to` is `from` + 30 days | events; classes and camps default only `from`, so their window is open-ended |
| `min_confidence` | `0.7` (`0.5` on places) | events, classes, camps, places |
| `audience` | everything except `adults` | events, classes, camps, places |

A request with none of them sent therefore sees one month of events and leaves
out lower-confidence and adult-audience listings. That is the right default for
a family product, but it is easy to forget when you compare a list against a
market-wide total.

Every list response says what it used. `meta.applied_filters` holds the value in
effect for each filter the endpoint applies, and `meta.defaults_applied` names
the ones that came from a default rather than from you:

```bash
curl -sS -H "X-API-Key: $TAILEND_API_KEY" --get https://api.tailend.dev/v1/philadelphia/events \
  --data-urlencode "lat=39.9526" \
  --data-urlencode "lng=-75.1652" \
  --data-urlencode "radius_m=8000" \
  --data-urlencode "limit=1"
```

```json
{
  "meta": {
    "applied_filters": {
      "from": "2026-09-30",
      "to": "2026-10-30",
      "min_confidence": 0.7,
      "audience": ["not_adults"],
      "lat": 39.9526,
      "lng": -75.1652,
      "radius_m": 8000
    },
    "defaults_applied": ["from", "to", "min_confidence", "audience"]
  }
}
```

To widen the view, send the filter yourself: `to=2027-09-29` for a year of
events, `min_confidence=0` for everything regardless of confidence,
`audience=all` to include adult listings. The echo then shows your value and
drops the name from `defaults_applied`. The echoed value is always one you can
send back: `audience=not_adults` (the default) returns exactly the set you got
without the parameter.

The same two fields appear on events, classes, camps, places, `/search`,
`/nearby` and `/around`. Only filters that endpoint applies are listed, so a
`weekday` sent to `/camps` does not appear: the endpoint ignored it.

## Ask for the counts

`facets` takes any of `category`, `weekday`, `time_block`, `price_band`,
`age_band` and `organization`, comma-separated. Add `limit=0` when you only
want the counts:

```bash
curl -sS -H "X-API-Key: $TAILEND_API_KEY" --get https://api.tailend.dev/v1/philadelphia/classes \
  --data-urlencode "lat=39.9526" \
  --data-urlencode "lng=-75.1652" \
  --data-urlencode "radius_m=8000" \
  --data-urlencode "facets=category,weekday,time_block,price_band,age_band,organization" \
  --data-urlencode "limit=0"
```

```javascript
const params = new URLSearchParams({
  lat: "39.9526",
  lng: "-75.1652",
  radius_m: "8000",
  facets: "category,weekday,time_block,price_band,age_band,organization",
  limit: "0",
});
const res = await fetch(`https://api.tailend.dev/v1/philadelphia/classes?${params}`, {
  headers: { "X-API-Key": process.env.TAILEND_API_KEY },
});
const { facets, meta } = await res.json();
console.log(facets.total, "classes;", meta.defaults_applied);
```

```json
{
  "data": [],
  "facets": {
    "total": 112,
    "is_complete": true,
    "category": [
      { "value": "arts_creative", "count": 38 },
      { "value": "sports_active_play", "count": 27 },
      { "value": "music_movement", "count": 19 },
      { "value": "unspecified", "count": 4 }
    ],
    "weekday": [
      { "value": "monday", "count": 31 },
      { "value": "tuesday", "count": 36 },
      { "value": "wednesday", "count": 29 },
      { "value": "thursday", "count": 33 },
      { "value": "friday", "count": 18 },
      { "value": "saturday", "count": 44 },
      { "value": "sunday", "count": 12 }
    ],
    "time_block": [
      { "value": "morning", "count": 41 },
      { "value": "afternoon", "count": 39 },
      { "value": "evening", "count": 28 },
      { "value": "unspecified", "count": 4 }
    ],
    "price_band": [
      { "value": "free", "count": 9 },
      { "value": "under_20", "count": 14 },
      { "value": "20_to_50", "count": 35 },
      { "value": "50_to_100", "count": 22 },
      { "value": "100_plus", "count": 17 },
      { "value": "unspecified", "count": 15 }
    ],
    "age_band": [
      { "value": "baby", "count": 14 },
      { "value": "toddler", "count": 33 },
      { "value": "young_kids", "count": 51 },
      { "value": "bigger_kids", "count": 39 },
      { "value": "unspecified", "count": 12 }
    ],
    "organization": {
      "distinct": 41,
      "top": [
        { "id": "c45b9b3a-45ab-4f9c-b2e2-986dcb921f25", "name": "Example Arts Center", "count": 9 }
      ],
      "other_count": 97,
      "unspecified_count": 6
    }
  },
  "meta": { "limit": 0, "count": 0, "has_more": false, "next_cursor": null }
}
```

(Numbers are illustrative.) Without `limit=0` you get a normal page in `data`
and the same `facets` beside it, both from one request.

### How to read the counts

- **`total`** is the number of listings that match your filters. It is the
  number you would reach by paging the list to the end.
- **A listing is counted in every bucket it belongs to.** A class that meets
  Monday and Wednesday is in both; a listing with two categories is in both. So
  `category`, `age_band` and a class's `weekday` can add up to more than
  `total`. `time_block` and `price_band` put each listing in exactly one bucket.
- **`unspecified`** counts listings that do not state that detail (no start
  time, no price, no age range). It appears only when it is above zero.
- **Buckets.** `weekday` is the local day of an event, or each day a class
  meets. `time_block` is `morning` (before 12:00), `afternoon` (12:00 to before
  17:00) or `evening` (17:00 on), from the local start time. `price_band` is
  `free` for exactly the listings `free=true` returns, so the bucket and the
  filter give the same number; every other listing is banded by its lowest
  stated price in US dollars: `under_20`, `20_to_50`, `50_to_100`, `100_plus`.
  A listing that is not free and states no price above zero is `unspecified`. `age_band` uses the same bands as `ages.bands` on a
  listing.
- **`organization`** lists the twenty organizations with the most listings.
  `other_count` covers the rest, and `unspecified_count` the listings with no
  organization.
- **Not every facet exists on every collection.** `weekday` is for events and
  classes; `time_block` and `price_band` are for events, classes and camps. Ask
  for one a collection has no data for and it is named in `facets.unavailable`
  instead of returning an empty list.

Facets answer the filters you send, so a market overview near a point is
the same request you would page, with `facets=` and `limit=0` added. Use the
counts to draw the overview, then fetch a page only for the bucket the person
opens.

## Limits

- `facets=` is available on every plan and counts as **one request**, however
  many listings it covers. A facets-only request returns no records.
- `page` and `cursor` do not change the counts.
- The organizations list is capped at twenty.
- Very large sets are counted up to a fixed bound. When a count covers only
  part of the set, or counting ran out of time, `facets.is_complete` is `false`.
  Narrow the filters (a metro-wide overview is comfortably inside the bound) and
  ask again.
- `limit=0` is accepted only together with `facets`; on its own it is `400`.
- `/search` does not take `facets`.

## Related

- [Quickstart](https://api.tailend.dev/docs/quickstart.md) — filters, pagination and search.
- [Pagination](https://api.tailend.dev/docs/pagination.md) — walking a list when you do want the records.
- [Audience & versioning](https://api.tailend.dev/docs/audience-and-versioning.md) — why `adults` is left out by default.
- [Plans & access](https://api.tailend.dev/docs/plans-and-access.md) — what each plan includes.
