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

  1. Sign up at /developers/signup. Your first team + key get created together.
  2. Copy the key (shown exactly once). Manage future keys at /dashboard/api-keys.
  3. Send it in the Authorization header. That's it.
Verify your keybash
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.

Option A — Bearer (recommended)http
Authorization: Bearer wl_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyy
Option B — Custom headerhttp
X-Api-Key: wl_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyy

Keys 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}.

RoleWhat they can do
OwnerFull control. Transfer ownership, delete the team, manage billing.
AdminManage members, invites, and keys. Edit team profile. No billing or deletion.
DeveloperCreate and revoke keys. See members and usage.
ViewerRead-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

Request headershttp
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: 1716392400

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

Node verificationjs
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

POST bodybash
{
  "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.

PlanRequests/minRequests/month
Free3010,000
Pro120250,000
Business6005,000,000
Response headershttp
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 47
X-Wanderlust-Plan: pro

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

Example responsebash
{
  "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 envelopebash
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded (120 requests/min on the pro plan).",
    "docs": "https://wanderlustapp.io/developers/docs#errors"
  }
}
HTTPcodeWhen
400invalid_requestA query parameter is missing or malformed.
401missing_api_keyNo Authorization or X-Api-Key header sent.
401invalid_api_keyKey is revoked, deleted, or doesn't exist.
404not_foundResource slug doesn't exist.
429rate_limitedPer-minute budget exceeded — read Retry-After.
500internal_errorSomething broke on our side. Safe to retry with backoff.

Endpoints

Everything you can call today, in one place.

GET/api/public/v1/health

Health check

Unauthenticated. Returns { status: "ok" }. Useful for smoke tests and uptime monitors.

curl https://wanderlustapp.io/api/public/v1/health
GET/api/public/v1/me

Inspect 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"
Responsebash
{
  "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"
}
GET/api/public/v1/countries

List countries

Paginated. Returns ISO codes, capital, region, area, languages, currencies, calling codes, and more.

pageoptionalinteger1-based page number. Default 1.
limitoptionalintegerResults per page, max 100. Default 25.
continentoptionalstringFilter by continent name (e.g. "Asia").
regionoptionalstringFilter by region (e.g. "Western Europe").
searchoptionalstringCase-insensitive substring match on name.
curl "https://wanderlustapp.io/api/public/v1/countries?continent=Asia&limit=5" \
  -H "Authorization: Bearer $WL_API_KEY"
GET/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"
GET/api/public/v1/cities

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

limitoptionalintegerMax rows returned. Default and max 1000.
curl "https://wanderlustapp.io/api/public/v1/cities?limit=100"
GET/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

Code samples

The fastest path is the official TypeScript SDK — autocomplete, typed responses, friendly errors. Plain HTTP works fine too.

TypeScript (SDK)ts
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);
JavaScript (fetch)js
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);
Python (requests)py
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"])
Go (net/http)go
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/sdk on 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.

Create my API key