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:

ProductEndpointWhat it doesResponse time
SearchPOST /searchFree-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
FeaturedPOST /featuredA 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 IntelligencePOST /local-intelligence/streamAsk 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:

  1. GET /meta — learn the supported cities and the category, type and vibe vocabularies.
  2. GET /catalog — see how many experiences and venues each city currently holds.
  3. POST /search — retrieve results and render them; omit query to browse a slice by its constraints alone (section 5.2).
  4. POST /featured — surface the curated featured pool for a location, no query needed (Growth plan and above).
  5. POST /local-intelligence — move up to it once you can describe the person and the occasion in a sentence.
  6. 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:

ScopeEndpoints 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_windows are expressed in the local time of the target location as naive ISO 8601 datetimes (2026-09-05T19:00). The time zone is implied by location, so do not send a UTC offset or Z; such values are rejected.
  • A window's end is 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"}. local is wall-clock time at the venue; timezone is the IANA zone to interpret it in.
  • Audit timestamps (first_seen_at, last_seen_at, observed_at, image expires_at) are UTC and always end in Z (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..."
}
  • /search and /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+ carry meta.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"
  }
}
FieldMeaning
citiesThe exact values accepted by location.city.
categories, experience_types, vibe_tagsControlled vocabularies used in experience results.
limitsHard caps on request parameters (sections 5–7 restate them per field).
media_policyimage_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_intensityThe 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"]}}
}
FieldTypeDefaultNotes
querystring, max 1000 charsoptionalFree text. Omit it (or send null) to browse — section 5.2. A blank or whitespace-only string is rejected.
locationobjectrequiredExactly one of city or point.
location.citystring—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_textstring, ≤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_windowsarray, ≤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_typesarray["experiences","venues"]Any non-empty subset.
limitinteger 1–6010Maximum results per result type.
cursorstring—The meta.next_cursor of the previous page, everything else unchanged (section 3.5).
rankingobjectsee belowOrdering preferences.
ranking.prefer_popularitybooleanfalseMove 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_credibilitybooleanfalseSame lift on scores.credibility.
ranking.prefer_uniquenessbooleanfalseSame lift on scores.uniqueness.
mediaobjectsee belowControls how much media each result carries.
media.images_per_resultinteger 0–103
media.videos_per_resultinteger 0–108
media.include_videosbooleantrue
media.include_ai_generatedbooleantrueSet false to receive only photographs.
filtersobject—Narrows the experiences by exact attributes; venues are never filtered. Allowed only when result_types includes experiences.
filters.experiences.is_freeboolean—
filters.experiences.environment_typesarray 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_answer stays {}. 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"
}
FieldMeaning
result_typesThe result types this response contains.
experiences, venuesResult arrays, ordered by relevance. A type you did not request is an empty array.
meta.normalizedHow the request was interpreted: the resolved city or point, the effective area, the merged time windows, and the filters applied.
meta.normalized.window_mapFor 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_scopePresent (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_relaxedPresent (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.countsNumber of results per type.
meta.filteredPresent 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.retrievalPresent 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_strategyDiagnostic, 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_msServer processing time.
meta.credits_chargedCredits billed for this call (section 10).
meta.next_cursorOpaque cursor for the next page, or null on the last one (section 3.5).
meta.paginationOn 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"
}
FieldTypeDefaultNotes
locationobjectrequiredSame rules as /search.
time_windowsarray, ≤50[]Same rules as /search.
limitinteger 1–10020
cursorstring—The meta.next_cursor of the previous page, everything else unchanged (section 3.5).
sortscore or timescorescore: strongest first. time: soonest first.
mediaobjectsame 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"
}
FieldTypeDefaultNotes
messagestring, 1–2000 charsrequiredWhat 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.
placestring, ≤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, lngnumber—Their position, when you have it. Beats place.
radius_minumber 0.1–503 from a point, 5 from a named areaHow far they will go.
whenarray 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[].endstring—Give it only when there is a real deadline. An absent end means the rest of that day, not the coming year.
contextobject—What is true of this person across visits: interests (≤20 strings) and preferences. It reorders the answer; it never narrows what is searched.
efforthigh, medium, lowhighHow hard to think about the request (section 7.4).
limitinteger 1–40050How many results to return. A sentence is written for every one, and the price follows that — ask for what you will actually show.
cursorstring—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 place with "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"
}
FieldMeaning
understoodThe 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[].reasonOne 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[].score0–1, how well this result answers the request.
results[].reasonsThe same fit in short phrases — "you can talk", "sit down" — for a card that shows tags rather than prose.
results[].whyThe matched dimensions in their raw form. Diagnostic; may change.
results[].unknownDimensions the catalogue could not settle for this result. A silence, not a no.
results[].ongoingtrue for a run already open — a standing exhibition. Say "on now, through …" rather than a start time.
results[].image, imagesThe 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_rangeHow many rows were in the window and area before ranking. This is the figure understood quotes.
window, placeWhat the time and place words were resolved to. window.label is said in words, and place.mode is point, area or city.
assumptionsEach 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_areatrue 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_answertrue when nothing in range actually fits. The results are still returned, but say so rather than letting a weak match pass for a recommendation.
foldedResults 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_asideHow many rows in range were set aside as not somewhere to go — a deadline, an exam, a board meeting.
meta.credits_chargedWhat 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.

EventDataNotes
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.

effortUse it forFirst 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
mediumA request you have measured and want cheaper.6–9 credits
lowA 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 parameterTypeDefault
imagesinteger 0–103
include_videosbooleantrue
videosinteger 0–108
include_aibooleantrue

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 parameterTypeDefault
imagesinteger 0–103
include_aibooleantrue

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
}
FieldTypeMeaning
idUUIDStable identifier; use it with /experiences/{id}.
titlestring
summarystring or nullA short description suitable for display.
categorystring or nullOne of /meta categories.
experience_typestring or nullOne of /meta experience_types.
tagsarray of stringsFree-form descriptive tags.
vibe_tagsarray of stringsDrawn from /meta vibe_tags.
statusstringThe 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.typestringPrecision 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.timezonestringIANA zone of the venue.
time.next_starttime object or nullStart 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_endtime object or nullEnd 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_timeslotsarray, ≤10Occurrences 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_rulestring or nulliCalendar RRULE describing the repeat pattern (for example FREQ=WEEKLY;BYDAY=TH), when known.
venueobject or nullThe 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_onlineboolean
environment_typeindoor, outdoor, mixed or null
neighborhood, citystring or null
priceobject or nullmin, max (numbers or null), currency, is_free. Null when no price is known.
scoresobjectpopularity, credibility, uniqueness: 0–1, higher is stronger, null when not scored. The ranking request preferences boost these.
featuredbooleanGrowth 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.
mediaobjectSee section 9.3.
booking.primary_linkstring or nullThe best page to book or learn more.
booking.primary_link_kindpage, listing or nullWhat 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_linksobjectPlatform → URL for every listing this experience was found on.
booking.registration_requiredyes, no, unknownWhether attending requires signing up in advance.
attribution.sourcesarray, ≥1{platform, url} for each source listing.
attribution.first_seen_at, last_seen_atUTC timestampWhen the experience first entered the database and when it was last confirmed.
hostobject or nullWho 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_fitstring or nullOne sentence on who the experience is for.
social_intensity_levellow, medium, high or nullHow socially demanding the experience is: low is quiet and self-contained, high means mingling with strangers.
distance_kmnumberPresent on point searches: distance from the search centre, in kilometres.
other_occurrencesobjectPresent when several catalogue entries share the same title: {count, venues, date_range} summarises the entries folded into this one (section 7.2).
match, low_confidenceLocal 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 }
FieldTypeMeaning
idUUIDUse with /venues/{id}.
name, address, lat, lng, neighborhood, cityCoordinates are null when not known precisely.
venue_typestring or nullPrimary type, for example bar, restaurant, coffee_shop, park, performing_arts_theater, art_gallery.
categoriesarray of stringsAdditional classifications.
ratingobject or nullvalue, count and source (for example google_places) of a public rating.
price_levelFree, $, $, $$, $$ or null
summarystring or nullShort description.
activity_tagsarray of stringsWhat people do there.
wheelchair_accessibleboolean, absent when unobservedGoogle Places' wheelchair-accessible-entrance verdict. Absent means no observation — prompt the user to check, never assume either way.
amenitiesobject, absent when unobservedAnswers 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.
hoursobject, absent when unknownweekday_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.periodsarray, absent when unknownOne 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_rangeobject, absent when unknownWhat 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_summarystring, absent when unknownGoogle'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.
phonestring, absent when unknownThe venue's public number, in national format.
business_statusstring, absent when unknownOPERATIONAL 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.
linksobjectwebsite, google_maps, booking, menu; each a URL or null.
mediaobjectSee section 9.3.
distance_kmnumberPresent on point searches.
matchobjectLocal 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. role is cover (the lead image, listed first) or gallery. expires_at is 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_generated flags images that were generated rather than photographed; pass media.include_ai_generated: false to omit them.
  • Videos are links to the hosting platform (watch_url to play, embed_url to embed, thumbnail_url for 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.

ClassEndpointsRate limit (set per contract)Monthly quota (set per contract)
Search/search, /featured, /experiences/{id}, /venues/{id}60 requests/minute unless your contract says otherwise500,000 requests unless your contract says otherwise
Intelligence/local-intelligence10 requests/minute unless your contract says otherwise5,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.

EndpointCredits
/search1 credit per 20 results delivered, rounded up; minimum 1. A full two-type page of 60 + 60 results costs 6.
/featured1 credit per 5 results delivered, rounded up; minimum 1.
/experiences/{id}, /venues/{id}1 credit.
/local-intelligencePriced 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

HTTPerror.codeWhenWhat to do
401unauthorizedMissing, invalid or expired API key.Check the header and the key.
402insufficient_creditsCredit enforcement is on and the balance cannot cover the request.Top up; do not retry automatically.
403forbidden_scopeThe key lacks the scope this endpoint needs, or the account is suspended.Use a key with the right scope, or contact us.
403forbidden_tierThe endpoint is not included in your plan (/featured needs Growth or above).Contact us to upgrade.
400invalid_cursorThe 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.
404not_foundNo experience or venue with that id.Drop the stored id.
410cursor_expiredThe 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.
422invalid_requestA field is missing, out of range, unknown, or inconsistent. message lists the first problems.Fix the request.
422unsupported_citylocation.city is not a supported city. message lists the supported ones.Use a /meta city.
429rate_limitedPer-minute limit exceeded, or the API is at capacity.Wait Retry-After seconds and retry.
429quota_exceededMonthly quota for this class reached.Wait for the monthly reset or contact us.
500internalAn 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, and author when present).
  • Booking. Send users to booking.primary_link. When booking.primary_link_kind is listing, label it as the organiser's calendar rather than a booking page. booking.registration_required tells you whether to prompt them to sign up in advance.
  • Times. Render time.*.local in time.*.timezone. Use time.type to decide the format: show a clock for exact_datetime, a date for date_only, a range for date_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 understood first, then the results in the order given, each with its own reason (null means 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. Show assumptions where they can be corrected.
  • Images expire (section 9.3). Do not store an image URL past its expires_at; store the item id and refresh.

13. Health

GET https://rtdb.syncso.com/partner/health returns {"status": "ok"} without authentication. Use it for uptime monitoring.