gmaps-sdk
v0.3.0
Published
TypeScript client for the gmaps.dev places API. Not affiliated with Google.
Readme
gmaps-sdk
Find businesses on the map from your code: bakeries in Curitiba, dentists near a point. Each business is a place, with its name, phones, website, address, rating and a link to it on Google Maps.
Start
Create a key at https://console.gmaps.dev/keys. Then:
npm i gmaps-sdkimport { createClient } from "gmaps-sdk";
const gmaps = createClient({ apiKey: process.env.GMAPS_API_KEY });
const result = await gmaps.places.search({
keywords: ["bakery"],
geo: { location: "Curitiba, PR" },
});
console.log(result.places.map((place) => place.identity.name));That search is free. It answers at once with the places we have already saved for that
area. New places come back in Portuguese unless you pass lang: "en". A saved place keeps
the language of the search that last collected it.
Guides: https://gmaps.dev/docs/sdk
When we have not saved enough yet
If the saved places do not fill limit (20 by default, up to 120), the search also
starts a collection to find the rest, and result.collection is that collection.
Each new place it finds costs 1 credit, and it stops at limit, so a search never costs
more than limit credits.
A collection takes a few minutes. Watch the new places arrive:
if (result.collection) {
for await (const frame of gmaps.watchCollection(result.collection.id)) {
if (frame.type === "place") console.log(frame.data);
}
}The loop ends when the collection does. Or run the same search again later: the new
places come back, and this time they are free. To wait inside the search itself, pass
wait: 60. The answer then holds for up to 60 seconds while the collection works.
When you are out of credits, or already running as many collections as your plan
allows, the search still answers with the saved places. result.collection is then
null, and result.collectionError says why.
What it costs
- A place we already saved: nothing.
place.meta.cachedistrue. - A new place a collection found for you: 1 credit.
- Emails for a new place, on plans that include them: 1 more credit per place, only when we find at least one.
- Searching, quoting, exporting and reading your usage: nothing.
const usage = await gmaps.usage.get();
console.log(usage.creditsRemaining); // 1000A whole city
For more than one page, ask for a collection. Quote it first: a quote is free and says how many places we have saved there and the most the collection can cost.
const ask = { keywords: ["dentist"], geo: { location: "Curitiba, PR" }, maxPlaces: 500 };
const quote = await gmaps.collections.quote(ask);
console.log(`${quote.placesKnown} places are free. The most it costs: ${quote.maxCost} credits.`);
const collection = await gmaps.collections.create(ask);A collection is safe to ask for twice. Pass your own key, such as the id of the job that asked, and a second call with that key gets back the same collection. Nothing starts or costs credits twice, even if the first call got no answer:
const collection = await gmaps.collections.create(ask, { idempotencyKey: "job-1042" });Without a key, the client makes one for each call. That keeps its own retries safe, but not one of yours after your process restarts.
Wait for it to end, then turn its places into a file. The link opens a csv, json or xlsx file without a key, for 24 hours:
for await (const frame of gmaps.watchCollection(collection.id)) {
if (frame.type === "progress") console.log(frame.data);
}
const file = await gmaps.exports.create({ collectionId: collection.id, format: "csv" });
console.log(file.url);On a paid plan, a webhook can tell your server when a collection ends, so nothing has to stay connected:
const hook = await gmaps.webhooks.create({ url: "https://example.com/gmaps" });Store hook.secret: we show it only once. Your server checks every request against it
before trusting it. Check the raw body, before any JSON parsing, and await the check:
import { verifyWebhook, type WebhookEvent } from "gmaps-sdk";
const raw = await request.text();
const signature = request.headers.get("x-gmaps-signature");
if (!(await verifyWebhook(process.env.GMAPS_WEBHOOK_SECRET, raw, signature))) {
return new Response("Not from gmaps.dev", { status: 400 });
}
const event: WebhookEvent = JSON.parse(raw);
if (event.type === "collection.completed") console.log(event.data.collection.stopReason);stopReason is null when a collection finished cleanly. Otherwise it says why it
stopped: daily_cap when you reached today's limit, quota when this month's credits ran
out, engine when we could not finish the searching, empty when the searches found
nothing.
Before you ship
- A place is a business listing. It is not permission to contact anyone. Check the law where you and the people you contact are before you send anything.
- When a business asks us to leave it out, we drop it from every search and export from then on. Files you saved before still have it, so search again instead of reusing an old file.
- Email addresses come only on paid plans, after your account accepts the acceptable use policy.
Errors
Every error is a GmapsError with a message that says what to do next, plus code,
status, requestId, retryAfter (seconds), details, messageKey, params and
idempotencyKey.
429 QUOTA_EXCEEDED:details.scopesays which limit:day,monthorconcurrency(too many collections at once). Wait for it to reset, or change plans.429 RATE_LIMIT_EXCEEDED: too many requests this minute. Retry afterretryAfterseconds.402 PLAN_GATE: the plan does not include that feature.details.upgradeTonames the plan that does.404 PLACE_NOT_FOUND: we have not saved that place yet. Search its area first.TIMEOUT,NETWORK_ERROR, or a5xxon a call that is not a read: the call may have run and been charged. Call again only if a second run is fine.- The same failure on
collections.create: call again with the sameidempotencyKey(error.idempotencyKeyhas it). If the first call started a collection, you get that collection back. 409 CONFLICToncollections.create: that key already started a collection for a different ask. Send the original ask, or use a new key.
Details
createClient() without options calls https://api.gmaps.dev/v1. Pass
{ baseUrl: "http://localhost:3000/v1" } to call a local API instead. Methods return
data, not the response envelope. The list of open-source projects we build on needs
no key: createClient().attributions.list().
The package uses standard fetch, has no runtime dependencies, and ships ESM JavaScript
and TypeScript declarations. Use Node 22+, Bun, or a browser with AbortSignal.any and
AbortSignal.timeout.
Each try at a call times out after 30 seconds, including reading the response body. A
search with wait gets its wait plus 15 seconds. Pass { timeoutMs: 5000, signal } to any
method to change that for one call.
If a call gets no answer, or an error on our side (a 5xx), the client sends it again about
1 second later, and again about 2 seconds after that. It does this only for calls that are
safe to run twice: reads, quotes, coverage checks, cancels, webhook changes and deletes,
and a new collection, which every try sends with the same idempotency key. A search, a new
export, a new webhook and a webhook test are never sent again this way, because the first
try may already have run, and a second could cost credits, send something twice, or make a
second export in your list. After a 408, 425 or 429 that asks for a wait of 10 seconds or
less, any call waits and goes again. Pass maxRetries: 0 to turn retries off.
The API catalog generates the methods and types. From the repository root, run
bun run sdk:gen to update them and bun run sdk:check to check for drift.
gmaps.dev is not affiliated with Google or Alphabet.
O gmaps.dev não tem vínculo com o Google nem com a Alphabet.
