SyncSo Partner API
Version 1 · Last updated 2026-09-10
The SyncSo Partner API gives your application real-time access to structured intelligence about experiences and venues across major US cities. Our data is continuously discovered, enriched, and refreshed, so your AI can search, understand, and act on what's happening in the real world.
Quick start — clone the starter repo, add your API key, and run a working example in minutes. Fifteen worked examples, five per endpoint.
Live demo — run every endpoint against the production API in your browser, no key required.
Three capabilities for your AI:
| Product | Endpoint | What it does | Response time |
|---|---|---|---|
| Search | POST /search | Free-text search over live experiences and venues, narrowed by location, time windows and filters. Returns a ranked list with images, times, prices and source attribution. Omit the query and the constraints become the intent — browse a slice of the catalogue with no search box at all. | 2–4 s |
| Featured | POST /featured | A curated pool of standout experiences in each city. Every entry has been individually scored and selected, so results can be shown without a query. Growth plan and above. | under 1 s |
| Local Intelligence | POST /local-intelligence/stream | Ask what someone should do, in the words they would use. Everything on in that window and area is read against the request and comes back ranked, each result with a sentence saying why it is there. | ~7 s to the opening line · ~11 s to the first result · ~13 s for a screen of 50 |
Supporting endpoints cover lookups by id, catalogue sizing, vocabulary discovery and usage reporting.
1. Getting started
Base URL: https://rtdb.syncso.com/partner/api/v1
All endpoints use HTTPS and JSON. Send Content-Type: application/json on POST requests. Responses are compressed when you send Accept-Encoding: gzip. Local Intelligence answers as a stream of JSON events by default (section 7.3).
A first request:
curl -s https://rtdb.syncso.com/partner/api/v1/search \
-H "Authorization: Bearer rtdb_live_..." \
-H "Content-Type: application/json" \
-d '{
"query": "live jazz",
"location": {"city": "New York"},
"time_windows": [{"start": "2026-09-05T19:00", "end": "2026-09-05T23:59"}],
"limit": 5
}'
Interactive explorer. https://rtdb.syncso.com/partner/docs lets you call every endpoint from the browser: click Authorize, paste your key, then use Try it out on any operation.
Recommended integration order:
GET /meta— learn the supported cities and the category, type and vibe vocabularies.GET /catalog— see how many experiences and venues each city currently holds.POST /search— retrieve results and render them; omitqueryto browse a slice by its constraints alone (section 5.2).POST /featured— surface the curated featured pool for a location, no query needed (Growth plan and above).POST /local-intelligence— move up to it once you can describe the person and the occasion in a sentence.GET /usage— monitor your month-to-date consumption.
2. Authentication
Every request must carry your API key in one of two headers:
Authorization: Bearer rtdb_live_0123456789abcdef0123456789abcdef
or
X-API-Key: rtdb_live_0123456789abcdef0123456789abcdef
Keys are issued by SyncSo and shown once at issue time. Keep them server-side; never embed them in client applications or browser code.
Scopes. Each key carries one or both scopes:
| Scope | Endpoints it unlocks |
|---|---|
search | /meta, /catalog, /search, /featured (Growth plan+), /experiences/{id}, /venues/{id} |
intelligence | /local-intelligence |
GET /usage works with any active key.
Plan tiers. Independent of scopes, every partner account sits on a plan
tier, ordered sandbox < standard < growth. Scopes say which
kind of endpoint a key may call; the tier says which plan features the
account includes. Most endpoints are available on every tier — the one
tier-gated surface today is POST /featured, which requires the Growth plan
or above: a key below that tier receives 403 forbidden_tier (section 11)
even when it carries the search scope. The featured field on experience
results (section 9.1) follows the same line: present for Growth and above,
absent below. Your tier is set when your account is provisioned; contact us
to upgrade.
Expiry. A key may carry an expiry date set at issue time. Requests with an expired key return 401.
3. Conventions
3.1 Request IDs
Every response carries an X-Request-Id header, and the bodies of metered endpoints repeat it as request_id. Log it, and quote it when contacting support about a specific call.
3.2 Times and time zones
- In requests,
time_windowsare expressed in the local time of the target location as naive ISO 8601 datetimes (2026-09-05T19:00). The time zone is implied bylocation, so do not send a UTC offset orZ; such values are rejected. - A window's
endis optional, and sending it is a different question:{"start": "2026-09-05T19:00"}— that day, from 19:00. Anything starting at or after that time on September 5th answers, however late it finishes: a 23:00 show running past midnight is a September 5th show. Use this for "tonight" or "Saturday", which name when someone is free rather than when they must be done. A window covers one day, so a span of days is a window per day.{"start": "2026-09-05T17:00", "end": "2026-09-05T22:00"}— a deadline. An event whose stated end binds the attendee (a performance, a screening) answers only if it finishes by then. One you can leave whenever you like (a market, an exhibition) still answers, because going for two hours and leaving is what was asked. Where we have not established which kind an event is, it answers — not knowing is never grounds for withholding it.
- In responses, every event time is an object
{"local": "2026-09-05T21:30:00", "timezone": "America/New_York"}.localis wall-clock time at the venue;timezoneis the IANA zone to interpret it in. - Audit timestamps (
first_seen_at,last_seen_at,observed_at, imageexpires_at) are UTC and always end inZ(2026-09-04T09:15:40Z).
3.3 Unknown fields
Request bodies are validated strictly. A misspelled or unrecognised field returns 422 invalid_request rather than being ignored, so a typo can never silently widen a search.
3.4 Error envelope
Every error response, whatever the status, has the same body:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded for search",
"request_id": "req_3f9c2a7b1d4e6f80"
},
"retry_after_s": 1
}
retry_after_s (and the matching Retry-After header) appears only on responses that are worth retrying after a wait. See section 11 for the full code list.
3.5 Pagination
/search, /featured and /local-intelligence page with an opaque cursor. Send your first request without one; when more results exist, the response carries meta.next_cursor. Send that value back as cursor — with the rest of the request unchanged — for the next page. meta.next_cursor is null on the last page.
{
"query": "live jazz",
"location": {"city": "New York"},
"limit": 10,
"cursor": "eyJzIjogIjk1ZDQ..."
}
/searchand/featured: the first page computes and freezes the full result set; every later page is a slice of that frozen set. Ordering never shifts between pages and a row never appears twice. Pages 2+ carrymeta.pagination: {"source": "snapshot"}./local-intelligence: later pages are served from the first page's ranking, so they are fast and no result is repeated across a boundary. Reading past what has been ranked extends it, and only then does a page cost what a first call does.
Every page is billed like a first page: per delivered row (section 10).
Cursors expire after 15 minutes. That is deliberate: the catalog is live — events sell out, start, get superseded — so an old page would serve rows we already know are stale. An expired or unknown cursor returns 410 cursor_expired. A cursor sent with a changed request body (anything other than cursor and limit), or one issued to another key or endpoint, returns 400 invalid_cursor. In both cases re-issue the original request rather than retrying the cursor. Treat the cursor as opaque: build nothing on its contents.
4. Discovery endpoints
These endpoints are free: they do not count toward rate limits, quotas or credits.
4.1 GET /meta
Returns the vocabularies and limits that shape every other request. Cache it and refresh it daily; new cities and tags appear here first.
{
"cities": ["Austin", "Chicago", "Los Angeles", "New York", "San Francisco"],
"categories": ["Arts & Theater", "Music", "Sports", "Film", "Food & Drink", "Festival", "Comedy", "Nightlife", "Community", "Tech", "Fitness & Wellness", "Other"],
"experience_types": ["concert", "exhibition", "workshop", "class", "tour", "festival", "show", "screening", "tasting", "meetup", "competition", "lecture", "party", "market", "other"],
"vibe_tags": ["chill", "energetic", "intimate", "social", "adventurous", "artsy", "romantic", "family-friendly", "upscale", "casual", "quirky", "inspiring", "festive", "cozy", "lively"],
"result_types": ["experiences", "venues"],
"limits": {
"max_limit_search": 60,
"max_limit_intelligence": 60,
"max_limit_featured": 100,
"max_time_windows": 50,
"max_time_window_span_days": 90,
"max_images_per_result": 10,
"max_videos_per_result": 10,
"max_query_chars": 1000,
"max_radius_km": 50.0
},
"media_policy": {
"image_urls_expire": true,
"video_is_link_only": true,
"ai_generated_images_labeled": true
},
"social_intensity": {
"values": ["low", "medium", "high"],
"coverage": 0.71,
"distribution": {"low": 0.38, "medium": 0.44, "high": 0.18},
"observed_at": "2026-09-04T15:10:22Z"
}
}
| Field | Meaning |
|---|---|
cities | The exact values accepted by location.city. |
categories, experience_types, vibe_tags | Controlled vocabularies used in experience results. |
limits | Hard caps on request parameters (sections 5–7 restate them per field). |
media_policy | image_urls_expire: image URLs carry an expires_at. video_is_link_only: videos are links to the hosting platform. ai_generated_images_labeled: every image object carries is_ai_generated; send media.include_ai_generated: false (include_ai=false on the lookup endpoints) to omit such images. |
social_intensity | The values social_intensity_level can take, the share of experiences that carry one, and their distribution. |
4.2 GET /catalog
See current experience and venue coverage by market. Use it to understand the available catalogue before integrating a city.
{
"totals": {"city": "__all__", "experiences": 48210, "venues": 21877},
"cities": [
{"city": "Austin", "experiences": 3120, "venues": 1904},
{"city": "Chicago", "experiences": 7415, "venues": 3611},
{"city": "Los Angeles", "experiences": 9982, "venues": 5140},
{"city": "New York", "experiences": 21104, "venues": 8455},
{"city": "San Francisco", "experiences": 6589, "venues": 2767}
],
"observed_at": "2026-09-04T15:02:41Z",
"stale": false
}
Counts are a snapshot taken at observed_at; stale: true means the snapshot is older than its normal refresh interval. Experiences and venues are counted independently and are not additive (an experience is hosted at a venue).
5. POST /search
Search naturally for anything your users want to find, from a specific experience to a place nearby at a certain time.
5.1 Request
{
"query": "rooftop cocktails with a view",
"location": {"city": "New York", "area_text": "Williamsburg"},
"time_windows": [
{"start": "2026-09-05T18:00", "end": "2026-09-05T23:59"},
{"start": "2026-09-06T18:00", "end": "2026-09-06T23:59"}
],
"result_types": ["experiences", "venues"],
"limit": 10,
"ranking": {"prefer_popularity": true},
"media": {"images_per_result": 3, "include_videos": true},
"filters": {"experiences": {"is_free": false, "environment_types": ["outdoor", "mixed"]}}
}
| Field | Type | Default | Notes |
|---|---|---|---|
query | string, max 1000 chars | optional | Free text. Omit it (or send null) to browse — section 5.2. A blank or whitespace-only string is rejected. |
location | object | required | Exactly one of city or point. |
location.city | string | — | One of the /meta cities. Common aliases (NYC, SF, LA) are accepted. A borough or well-known neighborhood (for example Brooklyn) resolves to its metro and narrows results to that area. |
location.area_text | string, ≤120 chars | — | A neighborhood or area to narrow a city search ("Wicker Park", "Mission District"). Only valid together with city. |
location.point | {lat, lng, radius_km} | — | Circle search. radius_km 0.1–50. Results include distance_km. |
time_windows | array, ≤50 | [] | Local-time windows (section 3.2). Each window covers the day its start names — send one per day. end is optional and means a deadline; omit it to ask about that whole day. When sent it must be after its start and in the future at the location's local time — a window that has already ended is rejected with 422. The overall span from the earliest start to the latest end you send may not exceed 90 days. Overlapping or touching windows are merged; a merged window keeps a deadline only if every window it absorbed had one. |
result_types | array | ["experiences","venues"] | Any non-empty subset. |
limit | integer 1–60 | 10 | Maximum results per result type. |
cursor | string | — | The meta.next_cursor of the previous page, everything else unchanged (section 3.5). |
ranking | object | see below | Ordering preferences. |
ranking.prefer_popularity | boolean | false | Move results whose scores.popularity is above the catalogue median earlier, in proportion to how far above it they are. A bounded lift, not a sort: relevance still carries most of the order, so a clearly better match keeps its place. Results at or below the median, or without a score, are left where they were. |
ranking.prefer_credibility | boolean | false | Same lift on scores.credibility. |
ranking.prefer_uniqueness | boolean | false | Same lift on scores.uniqueness. |
media | object | see below | Controls how much media each result carries. |
media.images_per_result | integer 0–10 | 3 | |
media.videos_per_result | integer 0–10 | 8 | |
media.include_videos | boolean | true | |
media.include_ai_generated | boolean | true | Set false to receive only photographs. |
filters | object | — | Narrows the experiences by exact attributes; venues are never filtered. Allowed only when result_types includes experiences. |
filters.experiences.is_free | boolean | — | |
filters.experiences.environment_types | array of indoor, outdoor, mixed | — | Must be non-empty when present. |
At least one of is_free or environment_types must be set when filters is present. A filter keeps only experiences whose attribute is recorded and matches: an experience whose attribute is unknown is left out rather than guessed.
Time windows and results. With time windows, an experience is returned when at least one of its occurrences overlaps a window, and its time.matched_timeslots lists those occurrences with the indices of the windows they fall in. Without time windows, matched_timeslots lists the experience's upcoming occurrences.
Availability. SyncSo does not publish live ticket inventory. An experience's status reflects its event lifecycle, not live ticket counts. Treat the booking provider as the source of truth for ticket availability.
5.2 Browse without a query
Omit query — or send null — and the constraints become the intent: a
location, optionally time windows, filters and ranking preferences describe
the slice you want, and /search returns what is in it. Use it for the
surfaces where nobody has typed anything: a "free and indoor this weekend"
module, a neighborhood page, a filter panel the user drives without ever
opening a search box. A blank or whitespace-only query is still rejected —
an empty search box forwarded verbatim is a bug on your side, so this fails
loudly rather than quietly returning everything.
{
"location": {"city": "New York"},
"time_windows": [{"start": "2026-09-12T17:00", "end": "2026-09-13T23:59"}],
"result_types": ["experiences"],
"filters": {"experiences": {"is_free": true, "environment_types": ["indoor"]}},
"limit": 5
}
Everything outside retrieval behaves exactly as it does with a query:
location and time-window semantics, filters, media, cursor pagination,
the response shape, and pricing at 1 credit per 20 results. Two differences
follow from there being no query to be relevant to:
- Ordering is intrinsic. With no query, relevance is absent from the
blend rather than counted as zero, and the remaining axes — popularity,
credibility, uniqueness — carry the whole order.
ranking.prefer_*is therefore the primary way to steer a browse page, not a tie-breaker. meta.no_answerstays{}. An empty browse page is a true statement about the slice you asked for, not a failure to understand you. The query-only machinery — reinterpretation (meta.retrieval), the relevance floor, area widening — does not run, so those fields are absent too.
Browse reads the full active catalogue on every plan. /featured (section 6)
is the other queryless surface and answers a different question: it returns a
small, individually curated pool per city rather than everything that fits, in
under a second, on the Growth plan and above.
5.3 Response
{
"result_types": ["experiences", "venues"],
"experiences": [ { "...": "experience object, section 9.1" } ],
"venues": [ { "...": "venue object, section 9.2" } ],
"meta": {
"normalized": {
"city": "New York",
"area_text": "Williamsburg",
"area_scope": {"experiences": "requested", "venues": "requested"},
"point": null,
"time_windows": [
{"start": "2026-09-05T18:00:00", "end": "2026-09-05T23:59:00"},
{"start": "2026-09-06T18:00:00", "end": "2026-09-06T23:59:00"}
],
"window_map": [0, 1],
"filters": {"experiences": {"is_free": false, "environment_types": ["mixed", "outdoor"]}}
},
"counts": {"experiences": 10, "venues": 7},
"filtered": {},
"no_answer": {},
"latency_ms": 412,
"credits_charged": 1
},
"request_id": "req_3f9c2a7b1d4e6f80"
}
| Field | Meaning |
|---|---|
result_types | The result types this response contains. |
experiences, venues | Result arrays, ordered by relevance. A type you did not request is an empty array. |
meta.normalized | How the request was interpreted: the resolved city or point, the effective area, the merged time windows, and the filters applied. |
meta.normalized.window_map | For each time_windows entry you sent (by position), the index of the merged window that absorbed it. matched_timeslots[].window_indices refer to merged windows; use this map to translate them back to your own slots. |
meta.normalized.area_scope | Present (per result type) whenever you sent area_text: "requested" means the rows honour the area; "relaxed" means the area matched nothing and that array was widened to the whole city, so its rows ignore area_text. Check it before presenting results as "in Williamsburg". |
meta.normalized.area_text_relaxed | Present (per result type) when the area you named had no matching results and the response was widened to the whole city. Equivalent to area_scope being "relaxed" for that type; kept for existing integrations. |
meta.counts | Number of results per type. |
meta.filtered | Present per result type when results were removed on purpose: relevance_floor_dropped (weakly relevant rows removed) and time_claim_conflict (rows whose stated times contradict your windows). Absent keys mean nothing was removed. |
meta.no_answer | {"experiences": true} / {"venues": true} when the catalogue holds no meaningful answer for that type; the corresponding array is empty. Distinguish this from a request whose results were filtered away. |
meta.retrieval | Present per result type when the query was reinterpreted to retrieve results (for example a typo corrected or a non-English query translated): {trigger, language, candidate_tokens, primary}. |
meta.retrieval_strategy | Diagnostic, per result type (experiences today): which vector retrieval ran. {"strategy": "exact_over_eligible", "eligible": N} means the rows that matched your location, windows and filters were ranked exactly by semantic similarity; {"strategy": "ann"} means the approximate vector index was used (always the case for a city-wide request without windows or filters; "overflow": true when the matching set was too large to rank exactly). Results are correct either way; safe to ignore. |
meta.latency_ms | Server processing time. |
meta.credits_charged | Credits billed for this call (section 10). |
meta.next_cursor | Opaque cursor for the next page, or null on the last one (section 3.5). |
meta.pagination | On pages 2+ only: {"source": "snapshot"} — the page is a slice of the first page's frozen result set. |
meta may carry additional informational keys. They are diagnostic and not part of the contract.
6. POST /featured
A curated pool of standout experiences in each city. Every entry has been individually scored and selected, so results can be shown without a query — send a location and optionally a time window, and use it for surfaces where nobody has typed anything yet: a home screen, a city guide, a "this weekend" module. The pool is deliberately small: a city with three featured events returns three, not a padded list. Responds in under a second, cached or not. Available on the Growth plan and above.
6.1 Request
{
"location": {"city": "Chicago"},
"time_windows": [{"start": "2026-09-06T00:00", "end": "2026-09-07T23:59"}],
"limit": 20,
"sort": "score"
}
| Field | Type | Default | Notes |
|---|---|---|---|
location | object | required | Same rules as /search. |
time_windows | array, ≤50 | [] | Same rules as /search. |
limit | integer 1–100 | 20 | |
cursor | string | — | The meta.next_cursor of the previous page, everything else unchanged (section 3.5). |
sort | score or time | score | score: strongest first. time: soonest first. |
media | object | same as /search |
6.2 Response
Same envelope as /search, restricted to experiences: result_types is ["experiences"], there is no venues array, and meta.normalized additionally echoes sort. A recurring or multi-date show occupies a single slot in the results. An empty experiences array means nothing in the curated pool matches the location and windows.
7. POST /local-intelligence
Ask what someone should do, in the words they would use, and get the evening back decided. Send the request as a sentence — who they are with, the occasion, the budget, what they want to avoid, anything they cannot do — and everything on in that window and that area is read against it. What comes back is ranked, with one sentence per result saying why it is there.
Requires the intelligence scope.
Stream it. POST /local-intelligence/stream sends the opening line as
soon as the request has been read and counted, then each result the moment
its place is decided and its sentence is written. Measured against production
on a 50-result answer: the opening line at about 7 seconds, the first result
at 11, the last at 13.
POST /local-intelligence returns the same answer as one JSON body in about
13 seconds, for a caller with nowhere to put a partial page — a batch job, or
a tool call that has to return once. The results and their order are
identical; what the stream buys is the 7 seconds before the first one.
Set your HTTP client's timeout to at least 60 seconds on either path. A hard request can run longer than these figures, and a timeout shorter than the answer is the one failure mode that looks like an outage.
Two things to know before you integrate:
- Do not split the request into several calls. One request is one call,
however many interests it names. "Art in the afternoon, dinner somewhere
lively, then live music" is one
message. - Do not re-rank or filter what comes back. Every candidate was read against this request — a thousand rows and more — and the order is that reading. Picking your own favourites out of the middle discards the only part of the work you cannot repeat from a list.
7.1 Request
{
"message": "Six friends in their late twenties want somewhere to celebrate a birthday this weekend. Two are vegan, one does not drink, one uses a wheelchair so step-free access is required. They want to sit together and talk, not stand in a crowd. Under $60 a head.",
"place": "Lower East Side",
"when": [
{"start": "2026-09-05T19:00", "end": "2026-09-05T23:59"},
{"start": "2026-09-06T19:00", "end": "2026-09-06T23:59"}
],
"limit": 10,
"effort": "high"
}
| Field | Type | Default | Notes |
|---|---|---|---|
message | string, 1–2000 chars | required | What they want, as a person would say it. A sentence or two, not keywords. Leave nothing out for being unsearchable: a wheelchair, an allergy, a dislike, "my parents are in their seventies and can't be on their feet long" are read and reasoned about, and they are the most useful thing you can send. Time and place can be said here too. |
place | string, ≤120 chars | — | A neighbourhood, borough, landmark, street address or transit line, when you want to be certain of it rather than leave it to the sentence. Beats any place named in message. |
lat, lng | number | — | Their position, when you have it. Beats place. |
radius_mi | number 0.1–50 | 3 from a point, 5 from a named area | How far they will go. |
when | array of {start, end} | — | Windows you have already resolved, New York local time, YYYY-MM-DDTHH:MM. Beats any time in message. A list because a calendar is a list: two windows with a gap between them do not search the gap. One window may be sent bare rather than wrapped in a list. |
when[].end | string | — | Give it only when there is a real deadline. An absent end means the rest of that day, not the coming year. |
context | object | — | What is true of this person across visits: interests (≤20 strings) and preferences. It reorders the answer; it never narrows what is searched. |
effort | high, medium, low | high | How hard to think about the request (section 7.4). |
limit | integer 1–400 | 50 | How many results to return. A sentence is written for every one, and the price follows that — ask for what you will actually show. |
cursor | string | — | The next_cursor of the previous page, everything else unchanged (section 3.5). |
Notes:
- Anything you send as a field beats what the sentence says, field by field: a
stated
placewith "jazz tonight" means jazz, tonight, there. The sentence fills the gaps the fields leave and never overwrites them. - Times and places said in words are resolved against the real clock and map — "tonight", "this weekend", "Saturday morning", "near Columbia", "along the L train". Use the fields instead when you already hold exact values.
- New York only for now. For anywhere else, say so rather than asking.
7.2 Response
{
"understood": "1,436 things on in the Lower East Side this weekend — looking for a seated birthday dinner six people can talk across, step-free and vegan-friendly.",
"total_in_range": 1436,
"results": [
{
"id": "091c15c0-912a-4df1-85b1-b729e5fc9d45",
"title": "Dinner at Kindred",
"score": 0.78,
"reason": "A seated room that takes groups of six, with a vegan tasting menu and step-free entry from the street.",
"start": "2026-09-05T19:30:00",
"end": null,
"ongoing": false,
"venue": "Kindred",
"neighborhood": "Lower East Side",
"distance_mi": 0.4,
"price_min": 48,
"price_max": null,
"is_free": false,
"link": "https://example.com/kindred/book",
"image": "https://cdn.example.com/kindred.jpg",
"images": ["https://cdn.example.com/kindred.jpg"],
"summary": "A small seasonal restaurant on Orchard Street.",
"category": "Food & Drink",
"experience_type": "dining",
"reasons": ["you can talk", "sit down", "step-free"],
"why": ["talk_during=freely", "physical=sedentary"],
"unknown": []
}
],
"window": {"start": "2026-09-05T19:00", "end": "2026-09-06T23:59", "label": "Saturday evening and Sunday evening"},
"place": {"label": "Lower East Side", "mode": "area", "radius_mi": 5},
"assumptions": ["I read \"under $60 a head\" as a ceiling per person, not for the table."],
"next_cursor": "eyJzIjoi…",
"meta": {"credits_charged": 13, "effort": "high", "results": 10},
"request_id": "req_9a1b2c3d4e5f6071"
}
| Field | Meaning |
|---|---|
understood | The opening line: how much was read and what the request was taken to mean. Show it first — it is the reader's one chance to correct you before reading on. |
results[].reason | One sentence, addressed to the end user, built only from facts in the result itself. Safe to display verbatim. null when none could be written in time: show the result without it rather than hiding it. |
results[].score | 0–1, how well this result answers the request. |
results[].reasons | The same fit in short phrases — "you can talk", "sit down" — for a card that shows tags rather than prose. |
results[].why | The matched dimensions in their raw form. Diagnostic; may change. |
results[].unknown | Dimensions the catalogue could not settle for this result. A silence, not a no. |
results[].ongoing | true for a run already open — a standing exhibition. Say "on now, through …" rather than a start time. |
results[].image, images | The cover image and the rest. Keep an image on its own line with a blank line under it, or many clients will not draw it. |
total_in_range | How many rows were in the window and area before ranking. This is the figure understood quotes. |
window, place | What the time and place words were resolved to. window.label is said in words, and place.mode is point, area or city. |
assumptions | Each inference the request left open, in one sentence. Show them where they can be corrected; a wrong reading is visible here rather than silent. |
out_of_area | true when the circle searched is empty but the catalogue is not — the person is outside the area we cover. Say so rather than reporting an empty evening. |
thin_answer | true when nothing in range actually fits. The results are still returned, but say so rather than letting a weak match pass for a recommendation. |
folded | Results left out because they were the same suggestion as one that is here. Not a list to render; a fold is a judgement, and this is its trace. |
set_aside | How many rows in range were set aside as not somewhere to go — a deadline, an exam, a board meeting. |
meta.credits_charged | What this answer cost (section 7.4). |
7.3 Streaming
POST /local-intelligence/stream answers with text/event-stream. Read it
with any Server-Sent Events client; each event carries one JSON object.
| Event | Data | Notes |
|---|---|---|
meta | {request_id, stream_version} | Always first. |
stage | {name} | Progress through reading, placing, searching, ranking, choosing. Informational. |
understood | {text} | The opening line, as soon as the request has been read and counted. |
result | {rank, item} | One per result, in rank order. item is the object of section 7.2 including its reason, so display it on arrival. |
result_patch | {id, reason} | Rare: fills in a reason that had to be sent as null. Update the result with that id. |
done | {request_id, …} | Always last on success. Carries the response of section 7.2 without results, including meta and next_cursor. |
error | {error} | Last instead of done; the error envelope of section 3.4. The HTTP status is 200 before the first event, so a failure part-way through arrives as an event, not a status. |
curl -N https://rtdb.syncso.com/partner/api/v1/local-intelligence/stream \
-H "Authorization: Bearer $RTDB_API_KEY" -H "Content-Type: application/json" \
-d '{"message": "live jazz tonight somewhere we can talk", "place": "East Village", "limit": 10}'
7.4 Effort and price
effort buys planning and the quality of the sentences. Retrieval is
identical at all three — every setting reads the same depth — so this is not
a thoroughness dial.
effort | Use it for | First 50 results |
|---|---|---|
high (default) | Everything, unless you have measured that you want it cheaper. It plans with the strongest model and writes the fullest reasons — and it is also the fastest whole answer, so lower it to spend fewer credits, never to go quicker. | 10–15 credits |
medium | A request you have measured and want cheaper. | 6–9 credits |
low | A bare "what's on tonight". | 4–6 credits |
Each further 50 results adds 3 credits at high, 2 at medium, 1 at low.
The price is the measured cost of the work the answer actually did, so two identical requests minutes apart can differ: the planner's prompt is cached after the first call, and the range above is that difference — the upper figure is a cold cache, the lower a warm one. A call is never charged above its setting's ceiling; past it we absorb the difference.
8. Lookups by id
Fetch a single experience or venue by the id returned in any result. Use these to refresh an item you stored earlier, or to obtain a fresh image URL after one has expired.
8.1 GET /experiences/{id}
| Query parameter | Type | Default |
|---|---|---|
images | integer 0–10 | 3 |
include_videos | boolean | true |
videos | integer 0–10 | 8 |
include_ai | boolean | true |
Returns the experience object (section 9.1) plus request_id and meta.credits_charged. time.matched_timeslots lists upcoming occurrences. An unknown id returns 404 not_found; an id that is not a UUID returns 422 invalid_request.
8.2 GET /venues/{id}
| Query parameter | Type | Default |
|---|---|---|
images | integer 0–10 | 3 |
include_ai | boolean | true |
Returns the venue object (section 9.2) plus request_id and meta.credits_charged.
9. Objects
9.1 Experience
Built for product surfaces.
Each experience can include the content, timing, location, pricing, media, source attribution, and action links your application needs to render a real-world experience.
{
"id": "5b0d5a4e-2c6a-4d0b-9a3e-1f7f1c2a9d10",
"title": "Late Set at The Cellar",
"summary": "An intimate late-night jazz set in a 60-seat basement room, with a full cocktail menu and table service.",
"category": "Music",
"experience_type": "concert",
"tags": ["jazz", "live music", "cocktails"],
"vibe_tags": ["intimate", "chill"],
"status": "active",
"time": {
"type": "exact_datetime",
"timezone": "America/New_York",
"next_start": {"local": "2026-09-05T21:30:00", "timezone": "America/New_York"},
"next_end": {"local": "2026-09-05T23:30:00", "timezone": "America/New_York"},
"matched_timeslots": [
{
"start": {"local": "2026-09-05T21:30:00", "timezone": "America/New_York"},
"end": {"local": "2026-09-05T23:30:00", "timezone": "America/New_York"},
"status": "scheduled",
"type": "exact_datetime",
"window_indices": [0]
}
],
"recurrence_rule": null
},
"venue": {
"id": "0c2f7e1a-8b3d-4a5e-9f60-7d1c2b3a4e5f",
"name": "The Cellar",
"address": "83 West 3rd St, New York, NY 10012",
"lat": 40.7301,
"lng": -73.9997,
"neighborhood": "Greenwich Village",
"city": "New York"
},
"is_online": false,
"environment_type": "indoor",
"neighborhood": "Greenwich Village",
"city": "New York",
"price": {"min": 25.0, "max": 35.0, "currency": "USD", "is_free": false},
"scores": {"popularity": 0.62, "credibility": 0.81, "uniqueness": 0.44},
"featured": true,
"media": {
"images": [
{
"url": "https://…",
"expires_at": "2026-09-11T20:00:00Z",
"role": "cover",
"is_ai_generated": false,
"attribution": {"platform": "eventbrite", "author": null}
}
],
"videos": [
{"platform": "youtube", "watch_url": "https://…", "embed_url": "https://…", "thumbnail_url": "https://…"}
]
},
"booking": {
"primary_link": "https://…",
"primary_link_kind": "page",
"source_links": {"eventbrite": "https://…"},
"registration_required": "yes"
},
"attribution": {
"sources": [{"platform": "eventbrite", "url": "https://…"}],
"first_seen_at": "2026-08-20T14:02:11Z",
"last_seen_at": "2026-09-04T09:15:40Z"
},
"host": {"name": "The Cellar", "role": "venue", "description": null, "active_upcoming_event_count": 12},
"audience_fit": "Jazz listeners who want a seated room rather than a bar with music in the background.",
"social_intensity_level": "low",
"distance_km": 1.24
}
| Field | Type | Meaning |
|---|---|---|
id | UUID | Stable identifier; use it with /experiences/{id}. |
title | string | |
summary | string or null | A short description suitable for display. |
category | string or null | One of /meta categories. |
experience_type | string or null | One of /meta experience_types. |
tags | array of strings | Free-form descriptive tags. |
vibe_tags | array of strings | Drawn from /meta vibe_tags. |
status | string | The event lifecycle: active, expired, cancelled, sold_out, archived, draft. Search returns active experiences; a lookup by id can return any state. This field is not ticket inventory: active does not mean tickets are available, and we do not publish ticket counts — check the booking link. |
time.type | string | Precision of the schedule: exact_datetime (a real clock time), date_only (a day, no time of day), date_range (a run of days), recurring, ongoing, tba. |
time.timezone | string | IANA zone of the venue. |
time.next_start | time object or null | Start of the next occurrence that has not yet ended. When the schedule holds several claims about the same date, the most precise one is used: an occurrence with a clock time over a date_only day, and either over a multi-day run (date_range, or a series stored as one span). Within the same precision, the earliest start; an occurrence already in progress counts, so next_start can be earlier than now — read next_end to tell an event in progress from one you missed. When the request carries time_windows, the pick is made among the occurrences that intersect them (the ones in matched_timeslots), so next_start is the occurrence that put the result on your page; only when every matching occurrence has already ended does it fall back to the schedule-wide next occurrence. |
time.next_end | time object or null | End of the same occurrence as next_start (null whenever next_start is null). For date_only occurrences it is the following midnight, matching that occurrence's end in matched_timeslots. |
time.matched_timeslots | array, ≤10 | Occurrences relevant to the request (section 5.1), soonest first. Without time windows only occurrences that have not yet ended are listed; when more than ten are relevant, the ten kept are chosen by the same precision rule as next_start (clocked occurrences before day-only placeholders before runs), then ordered by start. Each has start, end, status (scheduled, or tba while the time is unannounced), its own type, and, on windowed requests, window_indices. For date_only occurrences start/end span the whole day. |
time.recurrence_rule | string or null | iCalendar RRULE describing the repeat pattern (for example FREQ=WEEKLY;BYDAY=TH), when known. |
venue | object or null | The hosting venue: id, name, address, lat, lng, neighborhood, city, and wheelchair_accessible when observed (section 9.2). Null for online or venue-less experiences. |
is_online | boolean | |
environment_type | indoor, outdoor, mixed or null | |
neighborhood, city | string or null | |
price | object or null | min, max (numbers or null), currency, is_free. Null when no price is known. |
scores | object | popularity, credibility, uniqueness: 0–1, higher is stronger, null when not scored. The ranking request preferences boost these. |
featured | boolean | Growth plan and above. true when the experience is in the curated featured pool — the same individually scored and selected set POST /featured returns — so you can badge it in your own results. Featured experiences already rank a little earlier in /search for every plan; the field itself is published to Growth and above only, and is absent below that. |
media | object | See section 9.3. |
booking.primary_link | string or null | The best page to book or learn more. |
booking.primary_link_kind | page, listing or null | What that page is. page: the event's own page. listing: a calendar or programme page the event was found on — the event is on it, but the reader has to find it there. Null when we have not classified the page yet. |
booking.source_links | object | Platform → URL for every listing this experience was found on. |
booking.registration_required | yes, no, unknown | Whether attending requires signing up in advance. |
attribution.sources | array, ≥1 | {platform, url} for each source listing. |
attribution.first_seen_at, last_seen_at | UTC timestamp | When the experience first entered the database and when it was last confirmed. |
host | object or null | Who is behind the experience, when known: name, role, description, active_upcoming_event_count. role is one of organizer, presenter, performer, venue, institution — print it as the label (Performer: Robert Glasper, Venue: Film Forum). The description is written from the entity's own website when we could verify the site is theirs. |
audience_fit | string or null | One sentence on who the experience is for. |
social_intensity_level | low, medium, high or null | How socially demanding the experience is: low is quiet and self-contained, high means mingling with strangers. |
distance_km | number | Present on point searches: distance from the search centre, in kilometres. |
other_occurrences | object | Present when several catalogue entries share the same title: {count, venues, date_range} summarises the entries folded into this one (section 7.2). |
match, low_confidence | Local Intelligence only (section 7.2). |
9.2 Venue
Venues are first-class results.
Search can return structured information about the place itself, including location, ratings, accessibility, amenities, hours, media, and relevant links.
{
"id": "0c2f7e1a-8b3d-4a5e-9f60-7d1c2b3a4e5f",
"name": "The Cellar",
"venue_type": "cocktail_bar",
"categories": ["bar", "establishment", "point_of_interest"],
"address": "83 West 3rd St, New York, NY 10012",
"lat": 40.7301,
"lng": -73.9997,
"neighborhood": "Greenwich Village",
"city": "New York",
"rating": {"value": 4.6, "count": 1843, "source": "google_places"},
"price_level": "$quot;,
"summary": "A 60-seat basement jazz room with nightly sets and a serious cocktail list.",
"activity_tags": ["live_music", "cocktails", "date_night"],
"wheelchair_accessible": true,
"amenities": {"reservable": true, "outdoor_seating": false, "good_for_children": false},
"hours": {
"weekday_descriptions": ["Monday: 5:00 - 11:00 PM", "Tuesday: 5:00 - 11:00 PM"],
"open_now": true,
"periods": [{"day": "monday", "opens": "17:00", "closes": "23:00"}]
},
"price_range": {"currency": "USD", "min": 20, "max": 70},
"review_summary": "People say this seafood restaurant serves fresh fish…",
"phone": "(212) 252-5091",
"business_status": "OPERATIONAL",
"links": {
"website": "https://…",
"google_maps": "https://…",
"booking": "https://…",
"menu": null
},
"media": {"images": [], "videos": []},
"distance_km": 1.24
}
| Field | Type | Meaning |
|---|---|---|
id | UUID | Use with /venues/{id}. |
name, address, lat, lng, neighborhood, city | Coordinates are null when not known precisely. | |
venue_type | string or null | Primary type, for example bar, restaurant, coffee_shop, park, performing_arts_theater, art_gallery. |
categories | array of strings | Additional classifications. |
rating | object or null | value, count and source (for example google_places) of a public rating. |
price_level | Free, $, $, $$, $$ or null | |
summary | string or null | Short description. |
activity_tags | array of strings | What people do there. |
wheelchair_accessible | boolean, absent when unobserved | Google Places' wheelchair-accessible-entrance verdict. Absent means no observation — prompt the user to check, never assume either way. |
amenities | object, absent when unobserved | Answers to the questions people ask before going: reservable, outdoor_seating, good_for_children, good_for_groups, serves_vegetarian_food, restroom, delivery, takeout, dine_in. Each key is present only when it was actually observed, so a missing key means nobody looked — it does not mean false. |
hours | object, absent when unknown | weekday_descriptions, the opening hours as lines you can show a person ("Monday: 5:00 - 11:00 PM"); periods, the same hours as data you can compute with; and open_now when it was recorded. open_now is a snapshot from the last enrichment, not a live check — use periods when the difference matters. |
hours.periods | array, absent when unknown | One entry per opening span: {"day": "saturday", "opens": "17:00", "closes": "02:30"}. Days are named, not numbered, because Places counts from Sunday and most calendar libraries count from Monday. Times are 24-hour, in the venue's own timezone. A span running past midnight carries the next day's clock, so closes can read earlier than opens. A venue open around the clock has closes: null. |
price_range | object, absent when unknown | What a person actually spends: {"currency": "USD", "min": 20, "max": 70}. Published beside price_level, not instead of it — the bucket covers more venues, this is more specific. Either bound may be absent. |
review_summary | string, absent when unknown | Google's own synthesis of what reviewers say ("People say this seafood restaurant serves fresh fish…"). A summary over many reviews, never one person's words, and never review text. |
phone | string, absent when unknown | The venue's public number, in national format. |
business_status | string, absent when unknown | OPERATIONAL or CLOSED_TEMPORARILY. Venues Google records as permanently closed are not returned at all, so this never says CLOSED_PERMANENTLY — a CLOSED_TEMPORARILY venue is one worth mentioning is shut for now. |
links | object | website, google_maps, booking, menu; each a URL or null. |
media | object | See section 9.3. |
distance_km | number | Present on point searches. |
match | object | Local Intelligence only (section 7.2). |
9.3 Media
{
"images": [
{
"url": "https://…",
"expires_at": "2026-09-11T20:00:00Z",
"role": "cover",
"is_ai_generated": false,
"attribution": {"platform": "eventbrite", "author": "Jane Doe"}
}
],
"videos": [
{"platform": "youtube", "watch_url": "https://…", "embed_url": "https://…", "thumbnail_url": "https://…"}
]
}
- Images.
roleiscover(the lead image, listed first) orgallery.expires_atis the UTC instant after which the URL stops working, or null for a permanent URL. Fetch or copy the image before it expires, or request the item again (/experiences/{id},/venues/{id}) for a fresh URL.is_ai_generatedflags images that were generated rather than photographed; passmedia.include_ai_generated: falseto omit them. - Videos are links to the hosting platform (
watch_urlto play,embed_urlto embed,thumbnail_urlfor a poster frame). Video bytes are never served.
10. Rate limits, quotas and credits
10.1 Rate limits and monthly quotas
Endpoints fall into two classes. Each class has a per-minute rate limit and a monthly request quota, applied per API key for the rate limit and per account for the quota.
| Class | Endpoints | Rate limit (set per contract) | Monthly quota (set per contract) |
|---|---|---|---|
| Search | /search, /featured, /experiences/{id}, /venues/{id} | 60 requests/minute unless your contract says otherwise | 500,000 requests unless your contract says otherwise |
| Intelligence | /local-intelligence | 10 requests/minute unless your contract says otherwise | 5,000 requests unless your contract says otherwise |
Both the rate limit and the monthly quota are contract terms and differ between partners; sandbox keys get one tenth of the defaults. GET /usage always reports the figures that apply to your key.
The per-minute figure is both a burst allowance and a steady rate: you may send that many requests at once, and the allowance then refills continuously at that rate (at the figures above, one search request per second and one Local Intelligence request every six seconds). Exceeding it returns 429 rate_limited with Retry-After; so does a moment when the API as a whole is at capacity (Retry-After: 2), whatever your own rate. Exhausting the monthly quota returns 429 quota_exceeded; the quota resets on the first day of each calendar month (UTC).
/meta, /catalog, /usage and /partner/health do not count toward any limit.
10.2 Credits
Usage is metered in credits. Every metered response reports what it cost in meta.credits_charged.
| Endpoint | Credits |
|---|---|
/search | 1 credit per 20 results delivered, rounded up; minimum 1. A full two-type page of 60 + 60 results costs 6. |
/featured | 1 credit per 5 results delivered, rounded up; minimum 1. |
/experiences/{id}, /venues/{id} | 1 credit. |
/local-intelligence | Priced on the work the answer actually did, which depends on effort and on how many results you asked for: the first 50 land at 4–6 credits on low, 6–9 on medium and 10–15 on high, and each further 50 adds 1, 2 or 3. Section 7.4 explains the range. |
Results are billed as delivered, never as requested: a limit of 60 that returns 4 results costs 1 credit.
When credit enforcement is enabled for your account (credits.enforced in /usage), each request first reserves its maximum possible charge and settles to the actual charge when it completes. A request the balance cannot cover returns 402 insufficient_credits.
10.3 GET /usage
Month-to-date consumption for your account, the limits in force for the calling key, and your credit position.
{
"month": "2026-09",
"partner": "Acme Travel",
"requests": {"search": 1240, "intelligence": 96, "total": 1336},
"quotas": {"search": 500000, "intelligence": 5000},
"rpm_limits": {"search": 60, "intelligence": 10},
"credits": {
"balance": 48210,
"enforced": false,
"used_this_month": {"search": 1410, "intelligence": 612, "total": 2022},
"pricing": {
"search": "ceil(results_returned / 20), min 1",
"intelligence": "scaled to the work performed, min 1, max 20 per result kind (40 dual-kind)"
}
}
}
Request counts and credit usage are refreshed within about a minute. credits.balance is always current.
11. Errors
| HTTP | error.code | When | What to do |
|---|---|---|---|
| 401 | unauthorized | Missing, invalid or expired API key. | Check the header and the key. |
| 402 | insufficient_credits | Credit enforcement is on and the balance cannot cover the request. | Top up; do not retry automatically. |
| 403 | forbidden_scope | The key lacks the scope this endpoint needs, or the account is suspended. | Use a key with the right scope, or contact us. |
| 403 | forbidden_tier | The endpoint is not included in your plan (/featured needs Growth or above). | Contact us to upgrade. |
| 400 | invalid_cursor | The cursor is malformed, was issued to another key or endpoint, or came with a changed request body. | Re-issue the original request without a cursor. |
| 404 | not_found | No experience or venue with that id. | Drop the stored id. |
| 410 | cursor_expired | The cursor's page set has expired (cursors live 15 minutes) or is unknown. | Re-issue the original request without a cursor; do not retry the cursor. |
| 422 | invalid_request | A field is missing, out of range, unknown, or inconsistent. message lists the first problems. | Fix the request. |
| 422 | unsupported_city | location.city is not a supported city. message lists the supported ones. | Use a /meta city. |
| 429 | rate_limited | Per-minute limit exceeded, or the API is at capacity. | Wait Retry-After seconds and retry. |
| 429 | quota_exceeded | Monthly quota for this class reached. | Wait for the monthly reset or contact us. |
| 500 | internal | An unexpected server error. | Retry once; if it persists, contact support with the request_id. |
Treat any non-JSON response as a transport error and retry with backoff.
12. Displaying results
- Image credit. Where you display an image, credit
media.images[].attribution(platform, andauthorwhen present). - Booking. Send users to
booking.primary_link. Whenbooking.primary_link_kindislisting, label it as the organiser's calendar rather than a booking page.booking.registration_requiredtells you whether to prompt them to sign up in advance. - Times. Render
time.*.localintime.*.timezone. Usetime.typeto decide the format: show a clock forexact_datetime, a date fordate_only, a range fordate_range. - Availability. We do not publish ticket inventory. Never present an experience as having tickets available or sold out on our authority — send the user to the booking link, which is the only current answer.
- Local Intelligence. Show
understoodfirst, then the results in the order given, each with its ownreason(nullmeans no sentence could be written for that result, not that it is a weaker match). The sentence is already written for this request — rewriting it costs the reader the reasoning, and writing your own from the title alone loses what the result actually says. Showassumptionswhere they can be corrected. - Images expire (section 9.3). Do not store an image URL past its
expires_at; store the itemidand refresh.
13. Health
GET https://rtdb.syncso.com/partner/health returns {"status": "ok"} without authentication. Use it for uptime monitoring.