Authentication
Authentication
Every request to the Navvo integration API is authenticated with an API key. Keys are scoped, rate-limited, and tied to a project inside your organization.
Sending your key
Pass the key on every request using the x-api-key header:
curl "https://navvo.io/api/v1/search?q=pharmacy" \
-H "x-api-key: nvvo_YOUR_KEY"
For URLs that a browser fetches directly and cannot add headers to — most importantly map tiles and styles in Navvo Maps — pass the key as a query parameter instead. Both api_key and apiKey are accepted:
https://navvo.io/api/v1/tiles/style?style=standard&api_key=nvvo_YOUR_KEY
x-api-key so your key doesn't end up in logs or browser history.Key format
Navvo keys are prefixed and easy to recognize:
nvvo_<48 hex characters>
Only a hash of the key is stored server-side, together with the last four characters (for display). The plaintext is returned exactly once, when the key is created.
Scopes
Each key carries a set of scopes. A request to a scoped endpoint succeeds only if the key grants that scope. There are eleven scopes:
| Scope | Grants access to | Enabled by default? |
|---|---|---|
search | Search | ✅ |
geocode | Geocoding (incl. batch) | ✅ |
places | Places — details, hours, photos, reviews, categories, quests | ✅ |
routing | Routing, matrix, isochrone, commute & itinerary | ✅ |
traffic | Traffic & Incidents | ✅ |
telemetry | Telemetry | ✅ |
tiles | Map Tiles & Map Data | ✅ |
imagery | Imagery | ✅ |
geo | Geofencing & addressing / Plus Codes | ❌ (opt-in) |
insights | Navvo Insights privacy-safe aggregates | ❌ (opt-in) |
optimize | Route Optimization (OR-Tools) | ❌ (opt-in) |
New keys are created with eight of the eleven scopes enabled by default. The geo, insights, and optimize scopes are off by default — request them explicitly if your integration needs geofencing, analytics, or optimization.
The live, authoritative list is also available to signed-in developers at GET /developer/scopes (each scope with a label and description).
Obtaining a key
The fastest way to get a key is the Developer Portal at navvo.io/developer — a point-and-click console for organizations, projects, keys, live usage and a request sandbox. Everything below is also available as a REST API if you prefer to automate it.
API keys are created through the developer portal endpoints, which are protected by a user session token (a JWT, not an API key). The flow is: account → organization → project → key. Every organization, project and key is scoped to the user who created it — you only ever see and manage your own.
1. Create an account / sign in
curl -X POST "https://navvo.io/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"name": "Your Name",
"password": "a-strong-password-min-10-chars"
}'
curl -X POST "https://navvo.io/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "a-strong-password-min-10-chars"
}'
Both return an accessToken (a Bearer JWT) plus your user and session:
{
"user": { "id": "uuid", "email": "[email protected]", "name": "Your Name", "roles": ["user"] },
"sessionId": "uuid",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Send this token as Authorization: Bearer <accessToken> on all /developer/* and /auth/me requests.
developer role (or admin/super_admin). Enrollment is self-serve — call POST /developer/enroll (or GET /developer/access, which auto-enrolls on first read) with your Bearer token to grant your own account the role. It's idempotent and takes effect on the very next request, so you can go straight from register → enroll → create a key with no admin step.2. Create an organization
curl -X POST "https://navvo.io/api/v1/developer/organizations" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Logistics", "slug": "acme-logistics" }'
3. Create a project
curl -X POST "https://navvo.io/api/v1/developer/organizations/<orgId>/projects" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "name": "Driver App" }'
4. Create an API key
curl -X POST "https://navvo.io/api/v1/developer/api-keys" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{
"projectId": "<projectId>",
"label": "Driver App – production",
"plan": "starter",
"scopes": { "geo": true }
}'
The response contains your key in the key field — this is the only time it is shown:
{
"key": "nvvo_3f9c2a1e8b7d6c5f4a3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b",
"id": "uuid",
"label": "Driver App – production",
"lastFour": "9c8b",
"scopes": {
"search": true, "geocode": true, "places": true, "routing": true,
"traffic": true, "telemetry": true, "tiles": true, "imagery": true, "geo": true
},
"plan": "starter",
"quotaPerMinute": 255,
"quotaPerDay": 100000,
"unlimited": false,
"active": true
}
Every key object carries a read-only unlimited boolean. It is true only for a key Navvo has made unlimited. While it is true, the key keeps its quotaPerMinute and quotaPerDay, but they are not enforced. You can't set unlimited through the developer API: like any unknown body field, it is dropped from the request. Contact Navvo if you need an unlimited key.
Request body — POST /developer/api-keys
free, starter, growth, scale, enterprise. Defaults to free. (The legacy startup value now maps to starter.) When you omit explicit quotas, the key adopts the plan's default quotaPerMinute / quotaPerDay.{ scope: boolean } map merged over the defaults. Pass { "geo": true } to enable the opt-in scope, or { "telemetry": false } to disable one.1 to 255 (the self-serve cap). Defaults to the plan's limit (free: 60).1 to 10000000 (the self-serve cap). Defaults to the plan's limit (free: 5000).{ "referrers": [...], "ips": [...] }: the websites and IP addresses the key may be used from. See Restricting a key.Managing keys
All of these endpoints require your Authorization: Bearer <accessToken> session token and are scoped to your account.
| Method | Path | Purpose |
|---|---|---|
POST | /developer/enroll | Self-serve: grant your own account the developer role (idempotent). Returns { enrolled: true, roles }. |
GET | /developer/access | Check developer access — { developer: true, roles }; auto-enrolls on first read. |
GET | /developer/overview | Catalog of products, plans, and rate limits. |
GET | /developer/scopes | The authoritative scope catalog (label, description, defaults). |
GET | /developer/summary | Your counts (organizations, projects, keys, active keys) and requests today. |
GET | /developer/organizations | List your organizations. |
POST | /developer/organizations | Create an organization (body: name; slug optional, auto-generated). |
PATCH | /developer/organizations/:id | Rename an organization. |
DELETE | /developer/organizations/:id | Delete an organization (cascades to its projects and keys). |
GET | /developer/projects?organizationId=… | List projects. |
POST | /developer/organizations/:id/projects | Create a project under an organization (body: name). |
POST | /developer/projects | Create a project (flat form — body: name, organizationId). |
PATCH | /developer/projects/:id | Rename a project. |
DELETE | /developer/projects/:id | Delete a project (cascades to its keys). |
GET | /developer/api-keys?projectId=… | List keys. Shows lastFour, scopes, restrictions, plan, quotas, unlimited, active, and live usage — never the plaintext. |
POST | /developer/api-keys | Mint a key (see Obtaining a key). Returns the plaintext once. |
GET | /developer/api-keys/:id/usage | Live per-key usage: { id, label, active, plan, lastUsedAt, unlimited, minute: { used, limit }, day: { used, limit } }, the requests this minute and today (UTC) against the quota. limit is null for an unlimited key. |
GET | /developer/api-keys/:id/usage/monthly | Month-to-date usage for billing: { id, plan, periodStart, weightedRequests, rawRequests, includedRequests, overageRequests }. |
PATCH | /developer/api-keys/:id | Update a key's label, plan, scopes, restrictions, quotaPerMinute, quotaPerDay, or active. Scope changes are merged over the key's current scopes. A quota you send unchanged is kept as it is, even when Navvo set it above the self-serve cap. On an unlimited key, quotaPerMinute and quotaPerDay are ignored. |
POST | /developer/api-keys/:id/rotate | Generate a new secret and invalidate the old one. Returns the new plaintext once. |
DELETE | /developer/api-keys/:id | Permanently delete a key. |
POST | /developer/sandbox | Run a read-only test request as one of your keys, server-side (body: apiKeyId, path, method? GET|POST, body?). Powers the portal Sandbox. |
Disabling vs. deleting
To temporarily turn a key off (and back on), set active:
curl -X PATCH "https://navvo.io/api/v1/developer/api-keys/<keyId>" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "active": false }'
An inactive key is rejected with 403 Forbidden until you re-enable it. DELETE removes the key permanently.
Rotating a key
If a key is leaked or lost, rotate it — this mints a brand-new secret and immediately invalidates the previous one, keeping the same key id, scopes and quotas:
curl -X POST "https://navvo.io/api/v1/developer/api-keys/<keyId>/rotate" \
-H "Authorization: Bearer <accessToken>"
The response includes the new key in plaintext — again, shown only once. Deploy the new secret to your apps; the old one stops working immediately.
Restricting a key
You can lock a key to the places it is meant to be used from. A key carries two lists. Each list that has entries must match, and an empty list means no restriction of that kind:
| List | Use it for | Matched against |
|---|---|---|
referrers: allowed websites | Keys used in web pages (Navvo Maps, browser fetch) | The browser's Origin header, or its Referer when there is no Origin |
ips: allowed IP addresses | Keys used from your servers | The address the request comes from |
Set them in the Developer Portal (API keys → Edit), or with the API:
curl -X PATCH "https://navvo.io/api/v1/developer/api-keys/<keyId>" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "restrictions": { "referrers": ["example.com", "*.example.com", "localhost:3000"] } }'
Websites. Write a host, optionally with a port: example.com, app.example.com, localhost:3000. *.example.com covers every subdomain but not example.com itself, so list both when you need both. A scheme or path is accepted and dropped (https://example.com/* is stored as example.com). Without a port, any port matches. A request that names no website is refused. That covers requests with neither Origin nor Referer, such as those from a server, or from a browser whose privacy settings strip both.
IP addresses. Write addresses or CIDR ranges, IPv4 or IPv6: 203.0.113.7, 198.51.100.0/24, 2001:db8::/32.
Each list holds up to 50 entries. In a PATCH:
- a list you leave out keeps its entries;
[]empties a list;"restrictions": nullremoves both lists.
An invalid entry is refused with 400, and the message names it; nothing is silently dropped. Every key response carries restrictions with both lists, which are empty when the key is unrestricted.
A refused request returns 403 with a code, plus the website or IP address the request came from, so you can tell what to add:
{
"statusCode": 403,
"error": "Forbidden",
"code": "KEY_WEBSITE_NOT_ALLOWED",
"message": "This API key is not allowed from this website",
"details": { "website": "copycat.example" }
}
code | Meaning |
|---|---|
KEY_WEBSITE_NOT_ALLOWED | The key has allowed websites, and the request came from another site or named none (details.website is then null). |
KEY_IP_NOT_ALLOWED | The key has allowed IP addresses, and the request came from another address (details.ip). |
Restrictions are checked before scopes and quota, so a refused request does not count against the key's quota. The portal Sandbox runs on Navvo's servers on your behalf and does not apply them.
Origin and Referer themselves. It does not make the key a secret: a script outside a browser can send any header. Give a server-side integration its own key, restrict it by IP address, and keep it out of client code.Errors
| Status | Meaning |
|---|---|
401 Unauthorized | No key supplied where one is required, or the key you sent is unknown (never issued, or rotated away). |
403 Forbidden | The key is deactivated, is missing the required scope, or is used from a website or IP address it is not allowed from. Also returned with code QUOTA_EXCEEDED when the key is over its quota or the per-IP limit (never for an unlimited key). |
429 Too Many Requests | Your IP address sent too many requests to one endpoint (code RATE_LIMITED). Wait for the seconds in Retry-After. See Rate limits. |
401 on every endpoint, and a deactivated key returns 403. A key parameter that is present but blank (only spaces), or sent more than once, is also refused with 401; an empty api_key= counts as no key. Beyond that, a presented key must hold the scope the endpoint requires, be within quota, and be used from a website or IP address it allows. A scope-limited, quota-exhausted or restricted key returns 403 instead of falling through to anonymous access, even on deployments where key enforcement is otherwise relaxed. Rotating or deactivating a key from the developer portal takes effect immediately.See Errors for response bodies and Rate limits for quota details.