Navvo
API Reference

Search events

Report which search result a user opened and what they did next — consent-gated click and conversion events that measure and improve search.

Search events

Tell Navvo which search result a user opened (a click) and what they did with it next (a conversion, such as asking for directions, calling or saving). Navvo uses events to measure search quality: how often people open a result, how far down the list they click, and which searches lead nowhere. It uses them to find ranking problems too. Events are optional, and search works the same without them.

Events are consent-gated. Navvo accepts them only for searches made with an X-Navvo-Session header, which you send only after the user agrees. Search analytics & privacy explains the whole model.

Authentication: the same API key that ran the search. Event calls are not metered.


POST /search/events

curl -s -X POST "https://navvo.io/api/v1/search/events" \
  -H "x-api-key: nvvo_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      { "queryId": "AaDxmIYgiEC1I4ZdHCWiERmR", "type": "click", "name": "open_place",
        "placeId": "e3bacbd9-1e5d-474c-b84e-2fe7e1556541", "position": 1 },
      { "queryId": "AaDxmIYgiEC1I4ZdHCWiERmR", "type": "conversion", "name": "directions",
        "placeId": "e3bacbd9-1e5d-474c-b84e-2fe7e1556541" }
    ]
  }'

Which searches accept events

An event is accepted only when the search it points to was event-eligible:

  1. The GET /search request carried an X-Navvo-Session header, sent because the user consented.
  2. That request carried neither Sec-GPC: 1 nor DNT: 1.
  3. Neither the query nor its top result was in a sensitive category (health or religion). Navvo never links those searches to a session.
  4. You send the event within 7 days of the search, with the same API key the search used.

The search response doesn't say whether it was eligible, and you don't need to know. Send events for the searches where you sent a session id. Ineligible ones come back counted in rejected, and nothing else is affected.

Request body

events
array required
1–20 events (see below). The whole body can be at most 16 KB.

Each events[] element:

FieldTypeRequiredNotes
queryIdstringyesThe queryId from the GET /search response the user acted on.
typestringyesclick or conversion.
namestringyesThe action. See the table below.
placeIdstringone of the twoThe place's id from the result.
slugstringone of the twoThe place's slug, if you don't have its id. Navvo resolves it.
positionnumbernoThe 1-based rank of the result in Navvo's list: (page − 1) × limit + index + 1. This is informational, because Navvo works out the position itself from the results it returned.
uiPositionnumbernoThe 1-based position where the result appeared in your UI, if you mix in other rows such as saved places or addresses. Navvo uses it to study layout.
typenameSend it when the user…
clickopen_placeopens a result (its detail sheet or page).
conversiondirectionsasks for directions to it.
conversionnavigatestarts turn-by-turn navigation to it.
conversionsavesaves it to favourites or a list.
conversioncalltaps its phone number.
conversionwebsiteopens its website.
conversionshareshares it.

You can send a conversion without a click first, for example from a "Directions" button on the result row. Arrival events are not supported.

Response (202)

{ "accepted": 1, "rejected": 1, "reasons": { "not_eligible": 1 } }
FieldTypeDescription
acceptednumberEvents stored.
rejectednumberEvents dropped.
reasonsobjectPresent only when something was rejected: a count per reason code.
Reason codeMeaning
invalid_query_idThe queryId isn't valid for this call: it is malformed or altered, or the event was sent with a different API key from the one that ran the search (the id's signature covers the key).
expiredThe search is more than 7 days old.
key_mismatchThe search was made with an API key and the event without one, or the other way round.
not_eligibleThe search wasn't event-eligible: there was no session id, the browser sent a privacy signal, or the query was sensitive. Also returned for every event while the project's search analytics are switched off.
invalid_nameThat name isn't allowed for the type, for example arrive, or open_place as a conversion.
missing_placeNeither placeId nor slug was sent.
unknown_placeThe slug doesn't match a place.
unavailableEvent collection is temporarily unavailable.
A 202 means Navvo accepted the batch, and it is written within seconds. Sending the same event twice is harmless, because each (queryId, type, name, place) combination is stored once. Don't retry rejected events: retrying fixes none of the reasons above.

How events are used

  • Navvo joins each event to its search by queryId. A click counts toward search metrics only when the place was among the first 20 results that search returned.
  • Clicks on promoted or sponsored results are never used to learn ranking.
  • Events are kept for 90 days, and events whose search record never arrived are deleted after about 24 hours. If a project switches its search analytics off, Navvo stops accepting events for the project's keys within about a minute and deletes the events already stored. Search analytics & privacy lists what Navvo keeps and for how long.

Sending events from an app or browser

// Call when the user opens a result. `search` is the GET /search response the result came from.
function reportClick(search, place, index) {
  const position = (search.places.page - 1) * search.places.limit + index + 1;
  return fetch("https://navvo.io/api/v1/search/events", {
    method: "POST",
    keepalive: true, // survives the page navigating away
    headers: { "Content-Type": "application/json", "x-api-key": KEY },
    body: JSON.stringify({
      events: [{ queryId: search.queryId, type: "click", name: "open_place", placeId: place.id, position }],
    }),
  }).catch(() => {}); // analytics must never break your UI
}
  • Use fetch with keepalive: true. navigator.sendBeacon can't send the x-api-key header.
  • If your backend proxies search to keep your key secret, proxy events the same way, because an event must carry the key the search used.
  • A browser request without an API key is accepted only from Navvo's own sites. Third-party pages must send their key.
  • Don't send events for a search response that was served from a cache. Search responses carry Cache-Control: no-store for this reason.
  • If you collect several events (for example, when the page is hidden), send up to 20 in one request.

With the Navvo SDKs

The Web SDK (@navvo/maps-js 0.2) and the Node SDK (@navvo/sdk 0.2) send events for you with the key the search used, and the Web SDK uses keepalive:

// Browser: @navvo/maps-js
import { NavvoServices, createSearchSessionId } from "@navvo/maps-js";
const services = new NavvoServices({ apiKey: KEY });
const session = createSearchSessionId(); // only if your user agreed to it
const r = await services.search("rumi cafe", { session });
const hit = r.items[0];
await services.sendSearchEvent({ queryId: r.queryId!, type: "click", placeId: hit.id, position: hit.position });

// Server: @navvo/sdk
const res = await navvo.search("rumi cafe", { session });
await navvo.sendSearchEvent({ queryId: res.queryId!, type: "conversion", name: "directions", placeId, position });

In the Web SDK every result carries its 1-based position, page offset included. See the Node.js SDK guide for the server side.

Errors

StatusWhen
400The body isn't { "events": [...] } with 1–20 events, or an event is missing queryId, type or name, uses a type other than click/conversion, or has a field of the wrong type.
403A browser request without an API key came from a site other than Navvo's own.
413The body is larger than 16 KB.
429More than 60 requests per minute came from one client IP (RATE_LIMITED, with a Retry-After header in seconds). This limit doesn't apply to an unlimited key, for example a partner server that relays its users' events; the limits of 20 events and 16 KB per request still do.
Copyright © 2026