# List classes

> Published, active classes.

Source: https://api.tailend.dev/docs/api/list-classes

```http
GET https://api.tailend.dev/v1/{metro}/classes
X-API-Key: $TAILEND_API_KEY
```

Published, active classes. `from`/`to` select terms that **overlap** the window
(`term.starts_on <= to AND (term.ends_on IS NULL OR term.ends_on >= from)`);
ongoing programs (`schedule.type = 'ongoing'`) and terms with no dates
always match. Unlike events, `to` has no default here: with `from` alone
(default today) the window is open-ended, so next season's terms are
listed. Rows below `min_confidence` (**default `0.7`**) and `adults`
audiences are left out unless asked for; `meta.applied_filters` and
`meta.defaults_applied` echo what was applied. `facets=` adds counts over
the whole filtered set. Ordered by `term.starts_on` nulls last, then `name`.

**Sections.** A program with `sections` is matched by one section at a time: the weekday
(`schedule_day`, `weekday`), `age`, `start_after`/`start_before`, the dates and `season`,
`free`/`max_price` and `openings=true` must all be satisfied by the same section. A Saturday
section for ages 3-5 and a Sunday section for ages 9-12 do not match `schedule_day=saturday&age=10`.
A program with no sections is judged on its own fields. Filters decide which programs are
returned; they never trim a program's `sections`.

**Grouping.** When a provider publishes one program as several listings, the list shows a
single card for it, with the other listings under `sessions`. Cards are formed before
paging, so a page never splits one and `limit` counts cards. `expand=sessions` returns every
class on its own.

Authorization: send your key in the `X-API-Key` header. Available on every plan.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `metro` | string | yes | Metro slug from /v1/metros (e.g. philadelphia). |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | no | Fetch specific listings of this resource type in one call: up to 100 comma-separated listing UUIDs (ids=<id>,<id>). |
| `from` | string (date) | no | Local calendar date (metro timezone), inclusive. |
| `to` | string (date) | no | Local calendar date (metro timezone), inclusive; at most 366 days after from. |
| `starts_from` | string (date) | no | Classes only. |
| `starts_to` | string (date) | no | Classes only. |
| `min_confidence` | number | no | Drop rows whose confidence is below this. |
| `category` | array of string | no | Keep rows whose categories contain any of these. |
| `audience` | array of string | no | Keep rows whose audience is one of these. |
| `season` | array of string | no | Events, classes and camps only. |
| `occasion` | string | no | Events and classes tagged for a seasonal occasion. |
| `age` | integer | no | A child's age in years. |
| `neighborhood` | string | no | Neighborhood slug or free-text name (case-insensitive). |
| `lat` | number | no | Center latitude for a radius search. |
| `lng` | number | no | Center longitude for a radius search. |
| `radius_m` | integer | no | Radius in metres around lat/lng (or near_zip, where enabled). |
| `zip` | array of string | no | Beta — may not be enabled for all keys yet. |
| `near_zip` | string | no | Beta — may not be enabled for all keys yet. |
| `include_approximate` | boolean | no | Beta — may not be enabled for all keys yet. |
| `place_id` | string (uuid) | no | Keep rows held at this place (place.id). |
| `free` | boolean | no | Events, classes, and camps only; ignored for places. |
| `openings` | boolean | no | Classes only. |
| `indoor` | boolean | no | Filter on is_indoor, a fact about the linked place. |
| `outdoor` | boolean | no | Filter on is_outdoor, a fact about the linked place. |
| `sensory` | boolean | no | Filter on is_sensory_friendly (on events and classes, the linked place's; on places, the place's own). |
| `schedule_day` | string | no | Keep classes that meet on this weekday. |
| `enrollment_type` | string | no | Keep rows matching this enrollment type. |
| `changed_since` | string (date-time) | no | Sync mode. |
| `q` | string | no | Full-text search over the listing's name and search text (websearch syntax: storytime park, "drop in", art -camp). |
| `weekday` | array of string | no | Keep rows on any of these local days. |
| `start_after` | string | no | Local start time at or after this, HH:MM (or HH:MM:SS). |
| `start_before` | string | no | Local start time strictly before this, HH:MM; 09:00–12:00 is the morning and 12:00 belongs to the afternoon. |
| `max_price` | number | no | A budget. |
| `organization_id` | string (uuid) | no | Keep rows published by this organization (organization.id). |
| `fields` | array of string | no | Keep only these dotted paths on each item (name,place.name,price.is_free); id and type are always kept so a trimmed item stays addressable. |
| `expand` | array of string | no | Attach the full Place and/or the Organization behind each item under expanded, one extra read per expansion per page (not per row). |
| `include` | array of string | no | Opt into a block the lean default response leaves out. |
| `include_mode` | string | no | How an include= the key is not entitled to is handled. |
| `cursor` | string | no | Opaque cursor from meta.next_cursor. |
| `page` | integer | no | 1-based page number (browse mode). |
| `facets` | array of string | no | Counts over every listing that matches the filters, not just this page, returned as a top-level facets object beside data and meta. |
| `limit` | integer | no | Items per page, 1 to 200. |

## Example request

```bash
curl -sS \
  -H "X-API-Key: $TAILEND_API_KEY" \
  "https://api.tailend.dev/v1/philadelphia/classes"
```

## Responses

### 200

A page of classes.

```json
{
  "data": [
    {
      "id": "c57d9bae-6479-41c5-9176-f314fa4d31f4",
      "type": "class",
      "slug": "kids-pottery-classes-b3d835f7",
      "name": "Kids Pottery Classes",
      "description": "6-week pottery classes for kids ages 8-16, focusing on wheel-throwing and hand-building techniques.",
      "program": {
        "enrollment_type": "enrollment",
        "skill_level_notes": "all levels",
        "adult_participation": null,
        "what_youll_learn": null,
        "prerequisites": null
      },
      "term": {
        "name": null,
        "starts_on": "2026-09-29",
        "ends_on": "2026-12-15",
        "total_classes": null
      },
      "schedule": {
        "type": "session",
        "days": [
          "tuesday"
        ],
        "start_time": "16:00:00",
        "end_time": "18:00:00",
        "duration_minutes": 120,
        "is_recurring": true,
        "rrule": "FREQ=WEEKLY;BYDAY=TU",
        "notes": "Tuesdays (4:00 pm – 6:00 pm)",
        "timezone": "America/New_York"
      },
      "tuition": {
        "amount": 185,
        "from": 185,
        "text": "$185 for 6-week session",
        "drop_in_price": null,
        "trial_price": null,
        "trial_notes": null,
        "currency": "USD"
      },
      "price": {
        "type": "fixed",
        "min": 185,
        "max": 185,
        "scope": "admission",
        "note": "$185 for 6-week session",
        "unit": "per_session",
        "is_free": false,
        "currency": "USD"
      },
      "ages": {
        "min": 8,
        "max": 16,
        "text": "Early Elementary, Tweens, Teens",
        "audience": "kids",
        "bands": [
          "bigger_kids"
        ]
      },
      "audience": "teens",
      "traits": {
        "seasons": [
          "fall",
          "winter"
        ]
      },
      "place": {
        "id": "1af0824d-36cf-482d-a6d7-073343ae52ed",
        "slug": "earth-center-pottery-ebc6fdd7",
        "name": "Earth Center Pottery"
      },
      "location": {
        "address": "10 Stable Mill Trl, Richboro, PA 18954, USA",
        "street_address": "10 Stable Mill Trl, Richboro, PA 18954, USA",
        "city": "Richboro",
        "state": "PA",
        "zip": "18954",
        "lat": 40.2226282,
        "lng": -74.9866812,
        "neighborhood": "Somerton",
        "neighborhood_slug": "somerton"
      },
      "categories": [
        "arts_creative"
      ],
      "occasions": [],
      "registration": {
        "url": "https://www.cognitoforms.com/TylerParkCenterForTheArts/KidsPotteryClasses",
        "status": "open",
        "opens_at": null,
        "spots_remaining": null,
        "has_spots_available": true,
        "waitlist_available": false,
        "sections_with_spots": 1
      },
      "sections_total": 1,
      "sections": [
        {
          "id": "0b8a3f5e-52c4-4d64-9a0e-2f6d7c1e9a11",
          "name": "Tuesday afternoon",
          "schedule": {
            "days": [
              "tuesday"
            ],
            "start_time": "16:00:00",
            "end_time": "18:00:00",
            "duration_minutes": 120,
            "rrule": "FREQ=WEEKLY;BYDAY=TU",
            "notes": "Tuesdays (4:00 pm – 6:00 pm)",
            "timezone": "America/New_York"
          },
          "term": {
            "name": null,
            "starts_on": "2026-09-29",
            "ends_on": "2026-12-15",
            "total_classes": null
          },
          "ages": {
            "min": 8,
            "max": 16,
            "text": null
          },
          "tuition": {
            "amount": 185,
            "text": "$185 for 6-week session",
            "drop_in_price": null,
            "currency": "USD"
          },
          "registration": {
            "url": "https://www.cognitoforms.com/TylerParkCenterForTheArts/KidsPotteryClasses",
            "status": "open",
            "spots_remaining": null,
            "has_spots_available": true,
            "waitlist_available": false
          },
          "updated_at": "2026-09-26T12:01:08.782Z"
        }
      ],
      "sessions": [],
      "image_url": "https://earthcenterpottery.com/wp-content/uploads/2024/02/IMG_0318-scaled-e1707359564209-1024x566.jpg",
      "organization": {
        "id": "37bae48b-e86c-44a4-80d7-29fd9c34443c",
        "name": "Earth Center Pottery",
        "slug": "earthcenterpotterycom-f09a2fcc"
      },
      "confidence": 0.45,
      "verified_at": "2026-09-26T12:00:21.748Z",
      "metro": "philadelphia",
      "status": "published",
      "created_at": "2026-07-17T16:01:41.805Z",
      "updated_at": "2026-09-26T12:01:08.782Z",
      "links": {
        "self": "/v1/classes/c57d9bae-6479-41c5-9176-f314fa4d31f4",
        "place": "/v1/places/1af0824d-36cf-482d-a6d7-073343ae52ed",
        "organization": "/v1/organizations/37bae48b-e86c-44a4-80d7-29fd9c34443c"
      }
    }
  ],
  "meta": {
    "limit": 1,
    "count": 1,
    "has_more": false,
    "applied_filters": {
      "from": "2026-09-14",
      "to": null,
      "min_confidence": 0.7,
      "audience": [
        "not_adults"
      ]
    },
    "defaults_applied": [
      "from",
      "min_confidence",
      "audience"
    ]
  }
}
```

### 400

Malformed or contradictory parameters (lat without lng, to before from, unknown category), or a cursor this API did not issue for this exact request — replayed from another endpoint, resource, metro or filter set, hand-crafted, or altered.

```json
{
  "error": {
    "code": "bad_request",
    "message": "radius_m: must be at least 100",
    "details": {
      "param": "radius_m"
    },
    "request_id": "req_dfbc3295a8382b05"
  }
}
```

### 401

Missing, unknown or revoked X-API-Key.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "missing X-API-Key header",
    "request_id": "req_90e63908273f6bff"
  }
}
```

### 403

Two different reasons share this status.

```json
{
  "error": {
    "code": "forbidden",
    "message": "key is not scoped to metro 'boston'",
    "details": {
      "metros": [
        "philadelphia"
      ]
    },
    "request_id": "req_9e7a750acef48ab2"
  }
}
```

### 404

No published, active resource with that id (or unknown metro).

```json
{
  "error": {
    "code": "not_found",
    "message": "unknown metro 'boston'",
    "request_id": "req_7431b51e10ec2b77"
  }
}
```

### 405

The path exists; the method does not.

```json
{
  "error": {
    "code": "method_not_allowed",
    "message": "POST is not allowed on this path; this API is read-only",
    "details": {
      "allow": [
        "GET",
        "HEAD",
        "OPTIONS"
      ]
    },
    "request_id": "req_01J9Z3"
  }
}
```

### 429

A limit was reached.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "3 requests per minute exceeded",
    "request_id": "req_06bfd3c2d5e8aa9f"
  }
}
```

### 500

A bug on our side.

```json
{
  "error": {
    "code": "internal",
    "message": "internal error",
    "request_id": "req_01J9Z3"
  }
}
```

### 501

This endpoint or feature is not available and will not become available without a configuration change — for ?address=, this means no geocoder is configured.

```json
{
  "error": {
    "code": "not_implemented",
    "message": "this endpoint is not enabled on this deployment; it is not a fault and retrying will not help",
    "request_id": "req_01J9Z3"
  }
}
```

### 503

A transient outage: a dependency could not be reached or did not answer in time.

```json
{
  "error": {
    "code": "unavailable",
    "message": "the service is temporarily unable to serve this request; retry after the interval in Retry-After",
    "request_id": "req_01J9Z3"
  }
}
```
