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/searchconst params = new URLSearchParams({ q: "toddler story time", age: "4", near_zip: "19148", from: "2026-10-03", to: "2026-10-03" });
const res = await fetch(`https://api.tailend.dev/v1/philadelphia/search?${params}`, {
headers: { "X-API-Key": process.env.TAILEND_API_KEY },
});
const { data, meta } = await res.json();
for (const r of data) console.log(r.type, r.name, r.match, r.score);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.
swimfindsswimming. - Close relatives count. A short list of related family-activity words matches too:
potteryfindsceramics. - 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 theaudiencefilter 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:
match | Meaning |
|---|---|
all_terms | Every word you typed is in the listing's name or category, or prominent in the opening of its description. |
related | Every word is covered that way, but at least one only through a closely related word. |
partial | Only 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
sort | Order |
|---|---|
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. |
soonest | By an event's next occurrence; places and undated results last, ties broken by relevance. |
nearest | By distance. Requires lat + lng or near_zip. |
newest | Most recently added first. |
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/searchPaging and limits
- Search cursors are tied to the sort and the ranking mode. Keep the same
sortwhen you request the next page; changing it returns400. If the ranking mode changes between pages, you get400 bad_requestwithdetails.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 returns400. - Very broad queries may be capped, and
meta.cappedis thentrue. Add filters or a more specificqto narrow the results.
See Search every listing collection for every parameter.