Skip to content

Find listings

Search

One query across events, classes, camps and places, with match levels and scores.

Use GET /v1/{metro}/search when you do not know whether the answer is an event, a class, a camp or a place. One query searches all four, with the same visibility and filter rules as the individual list endpoints, so it never returns a listing those endpoints would hide.

curl -G -H "X-API-Key: $TAILEND_API_KEY" \
  --data-urlencode "q=toddler story time" \
  --data "age=4" --data "near_zip=19148" \
  --data "from=2026-10-03" --data "to=2026-10-03" \
  https://api.tailend.dev/v1/philadelphia/search

Each result keeps the shape of its own collection and adds snippet, match and score. Snippets are plain text, without highlight markup.

How your words are read

Search reads the query as plain words, and results can match by meaning as well as by keyword.

  • Word forms match. swim finds swimming.
  • Close relatives count. A short list of related family-activity words matches too: pottery finds ceramics.
  • Filler is ignored. Question and filler words such as "where can I", "with" or "for", and phrases such as "things to do", "near" and relative weekdays, are never required and never change a score.
  • Kind words only lift. Words that name the kind of listing ("class", "lessons", "camp") lift a listing that has them in its name, but never decide whether it matches.
  • Time and price words are not filters yet. "tonight" or "cheap" never decide a match; they come back in meta.ignored_words.
  • Audience words do not narrow. "parents" or "kids" in a longer query come back in meta.ignored_words; use the audience filter instead. A query made only of audience words ("Mommy and Me", "kids", "family") is searched as written.

A query with nothing searchable in it ("near me", "where can I") returns 400 bad_request.

Ages in the query

An age in the query becomes an age filter: "soccer for 5 year olds", "art ages 3-5", "toddlers" and "teens" all narrow by age. The band that was applied comes back in meta.interpreted.age, for example { "min": 1, "max": 3, "from": "toddlers" }.

Listings whose stated ages cover the band rank above listings with no stated ages, and listings whose ages exclude it are left out. An age or audience parameter you send yourself always wins: the band is not applied, and meta.interpreted.age.overridden_by names your parameter.

Match levels and score

Every result says how directly it matches in match:

matchMeaning
all_termsEvery word you typed is in the listing's name or category, or prominent in the opening of its description.
relatedEvery word is covered that way, but at least one only through a closely related word.
partialOnly some of your words, or all of them only mentioned in passing further down a description.

By default search returns all_terms and related results. Ask for loose matches with match=all_terms,related,partial (or match=partial alone). meta.match_counts reports how many results exist at each level whatever you selected, so you can show "17 direct, 36 related" and offer to widen. meta.count_estimate counts the levels you selected.

score is an absolute 0 to 1 measure of how well one listing fits the words of the query. It is not scaled against the other results: the same listing gets the same score whatever else comes back, and scores are comparable between queries.

meta.ranking is hybrid when meaning-based ranking contributed, and keyword when keyword ranking was used alone.

Sorting

sortOrder
relevance (default)all_terms, then related, then partial. Within a level a name match ranks above a category match, which ranks above a description match.
soonestBy an event's next occurrence; places and undated results last, ties broken by relevance.
nearestBy distance. Requires lat + lng or near_zip.
newestMost recently added first.
cURL
curl -G -H "X-API-Key: $TAILEND_API_KEY" \
  --data-urlencode "q=storytime" --data "sort=nearest" \
  --data "near_zip=19148" \
  https://api.tailend.dev/v1/philadelphia/search

Paging and limits

  • Search cursors are tied to the sort and the ranking mode. Keep the same sort when you request the next page; changing it returns 400. If the ranking mode changes between pages, you get 400 bad_request with details.reason: cursor_expired: restart the search without the cursor.
  • Search does not accept the collection paging and shaping parameters (page, include, fields, expand); sending one returns 400.
  • Very broad queries may be capped, and meta.capped is then true. Add filters or a more specific q to narrow the results.

See Search every listing collection for every parameter.