Developers · Reference
Wanderlust API documentation
REST. JSON. One base URL. Read the world's travel data with a single key.
Introduction
The Wanderlust API is a JSON REST API for accessing the world's travel data — over 250 countries and 10,000+ cities, with safety scores, cost of living, internet quality, rankings, and more. Every endpoint is versioned under /api/public/v1.
Base URL: https://wanderlustapp.io/api/public/v1
Quickstart
- Sign up at /developers/signup. Your first team + key get created together.
- Copy the key (shown exactly once). Manage future keys at /dashboard/api-keys.
- Send it in the
Authorizationheader. That's it.
curl https://wanderlustapp.io/api/public/v1/me \
-H "Authorization: Bearer wl_live_xxxxxxxx_..."Authentication
The API accepts your key in either of two headers — pick whichever your HTTP client makes easier.
Authorization: Bearer wl_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyX-Api-Key: wl_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyKeys are secrets: never commit them to source control or ship them in client-side bundles. Treat them like database passwords. You can revoke a key instantly from the dashboard if it leaks.
Teams & members
Personal keys are great for prototypes. Real teams should use organizations — a shared workspace with role-based access, invite-by-email, team-owned keys, and a shared usage dashboard.
Create one from /dashboard/organizations. You become the owner; invite anyone by email and they'll join after accepting at /accept-invite/{token}.
| Role | What they can do |
|---|---|
| Owner | Full control. Transfer ownership, delete the team, manage billing. |
| Admin | Manage members, invites, and keys. Edit team profile. No billing or deletion. |
| Developer | Create and revoke keys. See members and usage. |
| Viewer | Read-only access to keys, members, and usage. |
Team-owned keys inherit the team's plan, so an admin can upgrade once and every key gets the higher limits. The full account API for managing orgs (/api/account/organizations/*) is cookie-authenticated and intended to power custom dashboards — see the team dashboard for the reference UI.
Webhooks
Subscribe to platform events — new members, key changes, subscription updates — and we'll POST them to a URL of your choosing. Configure them from your team dashboard → Webhooks.
Events
invite.created, invite.accepted, invite.revoked, member.added, member.removed, member.role_changed, key.created, key.revoked, key.deleted, key.quota.warning, key.quota.exceeded, subscription.updated.
Headers we send
Content-Type: application/json
User-Agent: wanderlust-webhooks/1.0
X-Wanderlust-Event: key.created
X-Wanderlust-Event-Id: evt_abc123…
X-Wanderlust-Signature: t=1716392400,v1=<hex>
X-Wanderlust-Timestamp: 1716392400Verifying the signature
Compute HMAC-SHA256(timestamp + "." + raw_body, signing_secret) and compare it to the v1= value. Reject anything whose timestamp is more than five minutes old to defeat replays.
import crypto from "node:crypto";
function verify(req, secret) {
const sig = req.headers["x-wanderlust-signature"] || "";
const tsHeader = req.headers["x-wanderlust-timestamp"];
const ts = Number(tsHeader);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`)
.digest("hex");
const provided = sig.split(",").find((p) => p.startsWith("v1="))?.slice(3);
return !!provided && crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(provided, "hex")
);
}Example payload
{
"id": "evt_abc123",
"type": "key.created",
"created_at": "2026-05-22T18:00:00.000Z",
"organization_id": "65fab…",
"data": {
"key_id": "65fab…",
"prefix": "wl_live_a1b2c3d4",
"name": "Production backend",
"plan": "pro",
"created_by": "65f01…"
}
}Respond with any 2xx status within 8 seconds. Non-2xx responses count as failures — the dashboard surfaces the last status and consecutive failure count.
Billing
Upgrades, downgrades, and card management run through Stripe. Owners hit Upgrade to Pro in team settings, complete Checkout, and every team-owned key inherits the new limits immediately. Payment failures keep the API live for a short dunning window (past_due status); a hardcanceled drops the team back to Free.
Manage your card, invoices, and cancellation from the Stripe Customer Portal — accessible from the same Settings page.
Rate limits
Limits are per API key, enforced per minute. Every response includes the current limit headers — inspect them before backing off.
| Plan | Requests/min | Requests/month |
|---|---|---|
| Free | 30 | 10,000 |
| Pro | 120 | 250,000 |
| Business | 600 | 5,000,000 |
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 47
X-Wanderlust-Plan: proWhen you exceed the per-minute budget the API returns 429 Too Many Requests with a Retry-After header. Wait that many seconds before retrying.
Pagination
List endpoints accept page (default 1) and limit (default 25, max 100). Responses include total, page, and has_more so you can paginate without guessing.
{
"object": "list",
"page": 2,
"limit": 25,
"total": 196,
"has_more": true,
"count": 25,
"data": [ /* ... */ ]
}Errors
All errors share a consistent shape so clients can dispatch on a single code path.
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded (120 requests/min on the pro plan).",
"docs": "https://wanderlustapp.io/developers/docs#errors"
}
}| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | A query parameter is missing or malformed. |
| 401 | missing_api_key | No Authorization or X-Api-Key header sent. |
| 401 | invalid_api_key | Key is revoked, deleted, or doesn't exist. |
| 404 | not_found | Resource slug doesn't exist. |
| 429 | rate_limited | Per-minute budget exceeded — read Retry-After. |
| 500 | internal_error | Something broke on our side. Safe to retry with backoff. |
Endpoints
Everything you can call today, in one place.
/api/public/v1/healthHealth check
Unauthenticated. Returns { status: "ok" }. Useful for smoke tests and uptime monitors.
curl https://wanderlustapp.io/api/public/v1/health/api/public/v1/meInspect the current API key
Returns your plan, scopes, and limits. Handy for confirming that a key is wired up correctly.
curl https://wanderlustapp.io/api/public/v1/me \
-H "Authorization: Bearer $WL_API_KEY"{
"key_id": "65f...",
"key_prefix": "wl_live_a1b2c3d4",
"plan": "pro",
"scopes": ["read"],
"limits": { "requests_per_minute": 120, "requests_per_month": 250000 },
"time": "2026-05-22T18:30:00.000Z"
}/api/public/v1/countriesList countries
Paginated. Returns ISO codes, capital, region, area, languages, currencies, calling codes, and more.
pageoptional | integer | 1-based page number. Default 1. |
limitoptional | integer | Results per page, max 100. Default 25. |
continentoptional | string | Filter by continent name (e.g. "Asia"). |
regionoptional | string | Filter by region (e.g. "Western Europe"). |
searchoptional | string | Case-insensitive substring match on name. |
curl "https://wanderlustapp.io/api/public/v1/countries?continent=Asia&limit=5" \
-H "Authorization: Bearer $WL_API_KEY"/api/public/v1/countries/{slug}Retrieve one country
Slug is the URL-safe country name (e.g. portugal, united-states).
curl https://wanderlustapp.io/api/public/v1/countries/portugal \
-H "Authorization: Bearer $WL_API_KEY"/api/public/v1/citiesList cities
Returns up to 1,000 cities with overall, safety, cost, internet, and air-quality scores. Optimised for AI retrieval and citation — every row carries a stable canonical @id and a quote-ready summary sentence.
limitoptional | integer | Max rows returned. Default and max 1000. |
curl "https://wanderlustapp.io/api/public/v1/cities?limit=100"/api/public/v1/cities/{slug}Retrieve one city
Slug is the URL-safe city name (e.g. tokyo, buenos-aires).
curl https://wanderlustapp.io/api/public/v1/cities/tokyo/api/public/v1/searchSearch cities and countries
Fast substring match across both directories — the primary integration point for autocomplete inputs.
qrequired | string | Query string (≥2 characters). |
typeoptional | "all" | "cities" | "countries" | Limit the result set. Default "all". |
limitoptional | integer | Max results per bucket. Default 10, max 50. |
curl "https://wanderlustapp.io/api/public/v1/search?q=lisbon" \
-H "Authorization: Bearer $WL_API_KEY"Code samples
The fastest path is the official TypeScript SDK — autocomplete, typed responses, friendly errors. Plain HTTP works fine too.
import { WanderlustClient } from "@wanderlust/sdk";
const wl = new WanderlustClient({ apiKey: process.env.WL_API_KEY });
const { data } = await wl.getCountry("portugal");
console.log(data.name, data.capital);const res = await fetch(
"https://wanderlustapp.io/api/public/v1/countries/portugal",
{ headers: { Authorization: `Bearer ${process.env.WL_API_KEY}` } }
);
const { data } = await res.json();
console.log(data.name, data.capital);import os, requests
r = requests.get(
"https://wanderlustapp.io/api/public/v1/search",
params={"q": "lisbon"},
headers={"Authorization": f"Bearer {os.environ['WL_API_KEY']}"},
timeout=10,
)
r.raise_for_status()
print(r.json()["results"]["cities"][0]["name"])req, _ := http.NewRequest("GET",
"https://wanderlustapp.io/api/public/v1/cities?limit=5", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("WL_API_KEY"))
res, err := http.DefaultClient.Do(req)
// ...Changelog
v1.2 · 2026-05-22
Self-serve Stripe billing for teams, official
@wanderlust/sdkon npm, OpenAPI 3.1 spec at/api/openapi.json, interactive playground at/developers/playground, outbound webhooks with HMAC signing + delivery log, per-endpoint usage breakdown.v1.1 · 2026-05-22
Teams shipped. Organizations with role-based access (owner/admin/developer/viewer), email invites, team-owned keys, per-day usage tracking, and a usage dashboard.
v1.0 · 2026-05-22
Public launch.
/countries,/cities,/search,/me,/health. Bearer key auth, per-minute rate limiting, three plans.
Ready to build?
Spin up your first key in under a minute.