You're building the map page: markers, a store locator, a "search near me" box, maybe a geocode-on-submit form. You need coordinate-shaped JSON โ and every mainstream geocoder puts a key (and usually a credit card) between you and your first response. Verified with plain unauthenticated curls, September 2026:
{"status":"REQUEST_DENIED","error_message":"You must use an API keyโฆ"}. And a key means a Google Cloud project with billing enabled โ card on file before your first test call.{"message":"Not Authorized - No Token"} (HTTP 401). Tokens live behind account signup.{"status":{"code":401,"message":"missing API key"}}. Free tier exists, key still required first.Those keys and policies exist for good reasons โ real geocoding costs someone money. But if what you need today is deterministic coordinate-shaped data for a frontend, a demo, a workshop, or CI, this guide builds a hosted places API in one paste: no account, no key, CORS-open, HTTPS.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
-d '{"name":"fake-geo","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"name":"places",
"fields":[
{"name":"name","type":"company"},
{"name":"address","type":"address"},
{"name":"city","type":"city"},
{"name":"country","type":"country"},
{"name":"lat","type":"latitude"},
{"name":"lng","type":"longitude"},
{"name":"category","type":"oneOf","values":["restaurant","cafe","hotel","museum","park"]}
],
"seed":25
}'
latitude and longitude are first-class field types โ real numbers in range (โ90โฆ90, โ180โฆ180), not strings. A seeded place looks like this (live output; your values will differ):
{
"id": 2,
"name": "Northloop",
"address": "1756 Pine Ln",
"city": "Kyoto",
"country": "Nigeria",
"lat": -47.625583,
"lng": 3.21361,
"category": "cafe"
}
Honest note: seeded fields are independently random โ you'll get a Kyoto in Nigeria. That's fine for marker rendering and query logic; when a demo needs coherent pins, POST your own (section 3) or edit records in the dashboard.
Every list endpoint supports comparison suffixes on any numeric field โ which on lat/lng is a bounding-box query, the request every "search this area" map pane fires:
# viewport bbox: roughly Europe
curl "$BASE/m/$PID/places?lat_gte=35&lat_lte=60&lng_gte=-10&lng_lte=40"
# only cafรฉs, and only the fields your markers need
curl "$BASE/m/$PID/places?category=cafe&select=name,lat,lng"
# text search + pagination
curl "$BASE/m/$PID/places?q=north&page=1&limit=10"
# spreadsheet / QGIS import
curl "$BASE/m/$PID/places?mock_format=csv&select=name,lat,lng"
?select= keeps marker payloads small (id is always included); mock_format=csv returns RFC-4180 CSV of exactly those columns. Full parameter reference in the docs.
curl -s -X POST "$BASE/m/$PID/places" -H 'content-type: application/json' -d '{
"name":"Cafe Test","address":"Unter den Linden 1","city":"Berlin",
"country":"Germany","lat":52.5163,"lng":13.3777,"category":"cafe"
}'
# โ {"id":26,...} and GET /places/26 returns it โ writes are real
Unlike read-only sample APIs, this is a database: your "add location" form, admin CRUD, and optimistic-update logic all work against it, and the record is still there tomorrow.
/geocode endpointForms don't query lists โ they call a geocoder. Add a custom route that answers like one, echoing the submitted address back with fixed coordinates:
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"method":"GET","path":"/geocode","contentType":"application/json",
"body":"{\"query\":\"{{query.address}}\",\"results\":[{\"formatted\":\"{{query.address}}\",\"lat\":52.516275,\"lng\":13.377704,\"confidence\":9}],\"status\":\"OK\",\"ts\":\"{{now}}\"}"
}'
curl "$BASE/m/$PID/geocode?address=Brandenburg+Gate,+Berlin"
{
"query": "Brandenburg Gate, Berlin",
"results": [
{"formatted": "Brandenburg Gate, Berlin", "lat": 52.516275, "lng": 13.377704, "confidence": 9}
],
"status": "OK",
"ts": "2026-09-16T09:47:00.662Z"
}
Deterministic by design: it never turns an address into true coordinates โ it returns a stable, geocoder-shaped response your form handler can parse, in tests, forever, with zero rate limit anxiety. Need several fixture addresses with distinct coordinates? Add one route per path (/geocode/berlin, /geocode/tokyo) or keep fixtures as records and query ?address=โฆ exact-match on the places resource.
Code written against Google reads response.results and checks response.status. Reshape any list response into that envelope with a response-envelope template โ no client changes needed:
# template: {"results":"$data","status":"OK"} (URL-encoded below)
curl "$BASE/m/$PID/places?limit=2&mock_envelope=%7B%22results%22%3A%22%24data%22%2C%22status%22%3A%22OK%22%7D"
# โ {"results":[{...},{...}],"status":"OK"}
Set it project-wide with PUT /api/projects/$PID/settings {"envelope":โฆ} and every GET answers Google-shaped.
The geocoding failure modes worth testing are throttling and latency โ Google's OVER_QUERY_LIMIT, Nominatim's one-per-second policy, a slow third-party hop:
# deterministic retry drill: first two calls 429, third succeeds
curl -i "$BASE/m/$PID/places?mock_seq=429,429,200&mock_seq_key=geo1"
# a real rolling rate limit: 5 requests/min per client, then 429 + Retry-After
curl -i "$BASE/m/$PID/places?mock_ratelimit=5"
# the slow-geocoder spinner test
curl "$BASE/m/$PID/geocode?address=Berlin&mock_delay=2000"
All simulation flags (mock_status, mock_chaos, mock_jitter, sequences, snapshots) work on both resource endpoints and custom routes โ the full list is in the docs, sequences in their own section.
| You need | Real-world friction | Here |
|---|---|---|
| First test response | Google: API key + billing-enabled account; Mapbox/OpenCage: signup + token | One anonymous curl |
| Places/POI list for markers | Paid per-request Places APIs | Seeded places resource, yours to shape |
| Viewport / bbox query | Provider-specific syntax | lat_gte/lat_lte/lng_gte/lng_lte |
| Geocode form endpoint | Rate-limited, non-deterministic | Templated /geocode route, stable output |
| Hammering it from CI | Against Nominatim policy; costs money on Google | 10k req/project/day, that's what it's for |
| Google-shaped envelope | โ | mock_envelope template, byte-compatible results/status |
The clean split: Mockbird for map frontends, store-locator demos, form handlers, and CI. A real geocoder the moment a coordinate has to be true. They compose โ env base URL in dev, real API in prod.
places resource to the same project as your store list.The paste block in section 1 is the whole setup โ or open the dashboard and click it together. The same project also serves GraphQL, exports OpenAPI + TypeScript types, and ejects to db.json anytime โ no lock-in.