Search events
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:
- The
GET /searchrequest carried anX-Navvo-Sessionheader, sent because the user consented. - That request carried neither
Sec-GPC: 1norDNT: 1. - Neither the query nor its top result was in a sensitive category (health or religion). Navvo never links those searches to a session.
- 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
Each events[] element:
| Field | Type | Required | Notes |
|---|---|---|---|
queryId | string | yes | The queryId from the GET /search response the user acted on. |
type | string | yes | click or conversion. |
name | string | yes | The action. See the table below. |
placeId | string | one of the two | The place's id from the result. |
slug | string | one of the two | The place's slug, if you don't have its id. Navvo resolves it. |
position | number | no | The 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. |
uiPosition | number | no | The 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. |
type | name | Send it when the user… |
|---|---|---|
click | open_place | opens a result (its detail sheet or page). |
conversion | directions | asks for directions to it. |
conversion | navigate | starts turn-by-turn navigation to it. |
conversion | save | saves it to favourites or a list. |
conversion | call | taps its phone number. |
conversion | website | opens its website. |
conversion | share | shares 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 } }
| Field | Type | Description |
|---|---|---|
accepted | number | Events stored. |
rejected | number | Events dropped. |
reasons | object | Present only when something was rejected: a count per reason code. |
| Reason code | Meaning |
|---|---|
invalid_query_id | The 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). |
expired | The search is more than 7 days old. |
key_mismatch | The search was made with an API key and the event without one, or the other way round. |
not_eligible | The 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_name | That name isn't allowed for the type, for example arrive, or open_place as a conversion. |
missing_place | Neither placeId nor slug was sent. |
unknown_place | The slug doesn't match a place. |
unavailable | Event collection is temporarily unavailable. |
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
fetchwithkeepalive: true.navigator.sendBeaconcan't send thex-api-keyheader. - 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-storefor 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
| Status | When |
|---|---|
400 | The 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. |
403 | A browser request without an API key came from a site other than Navvo's own. |
413 | The body is larger than 16 KB. |
429 | More 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. |