SDK Reference
Auto-generated, type-safe SDKs for TypeScript and Go. Complete method reference for every API operation.
Installation & Setup
Install
npm install @r4mai/sdkInitialize
import { createClient } from "@r4mai/sdk";
const r4m = createClient({ apiKey: "acc_your_api_key_here" });Optimizations
Create Optimization
Submit addresses for route optimization. Returns immediately with a pending status (202). Include a webhookUrl to receive the result via webhook, or poll the get endpoint.
const { data: optimization, error } = await r4m.createOptimization({
body: {
addresses: [
{ address: "Depot", lat: 40.7128, lng: -74.006, isDepot: true, serviceTimeSeconds: 900 },
{ address: "Stop 1", lat: 40.758, lng: -73.9855, timeWindowStart: "09:00", timeWindowEnd: "11:00" },
{ address: "Stop 2", lat: 40.7484, lng: -73.9857 },
{ address: "Stop 3", lat: 40.7614, lng: -73.9776 },
],
parameters: {
travelMode: "driving",
optimizeMode: "Time",
roundTrip: false,
routeStartAt: "2026-04-14T08:00",
timezone: "America/New_York",
},
webhookUrl: "https://your-server.com/webhook",
},
});
if (error) throw error;
console.log(optimization.id, optimization.status); // "550e8400-..." "pending"List Optimizations
Retrieve optimizations with cursor-based pagination. Pass the nextCursor from the previous response to get the next page.
const { data: page, error } = await r4m.listOptimizations({ query: { limit: 20 } });
if (error) throw error;
for (const item of page.items) {
console.log(item.id, item.status, item.priceMicros);
}
// Next page
if (page.nextCursor) {
const next = await r4m.listOptimizations({
query: { cursor: page.nextCursor, limit: 20 },
});
}Get Optimization
Retrieve a single optimization by ID. When status is "completed", the result field contains the optimized route.
const { data: optimization, error } = await r4m.getOptimization({
path: { id: "550e8400-..." },
});
if (error) throw error;
console.log(optimization.status); // "completed"
const result = optimization.result;
if (result) {
// Always an array — one entry per vehicle, one entry for a single-vehicle run.
for (const route of result.routes) {
console.log(route.totalDistanceMeters); // meters
console.log(route.totalTimeSeconds); // seconds
// Driving order — waypoints are already in it; index points back at your
// addresses array.
console.log(route.waypoints.map((w) => w.index));
}
// Stops that could not be fitted at all. Always worth checking.
console.log(result.unroutedStops);
}Delete Optimization
Permanently delete an optimization and its result.
const { data: deleted, error } = await r4m.deleteOptimization({
path: { id: "550e8400-..." },
});
if (error) throw error;
console.log(deleted.id); // the id that was removedGeocoding
Forward Geocode
Convert a single address string to latitude/longitude coordinates.
const { data: geocoding, error } = await r4m.forwardGeocode({
body: { address: "1600 Amphitheatre Parkway, Mountain View, CA" },
});
if (error) throw error;
const result = geocoding.result;
if (result.status === "ok") {
console.log(result.lat, result.lng); // 37.4220, -122.0841
console.log(result.address, result.precision); // normalized address, "address"
} else {
console.log(result.status, result.candidates); // "ambiguous" | "not_found"
}Bulk Geocode
Geocode many addresses in a single request (up to your plan limit). One result per distinct address, in the order first sent: match by original, never by position. Priced per distinct address, charged once for the entire batch.
const { data: geocoding, error } = await r4m.bulkGeocode({
body: {
addresses: [
"1600 Amphitheatre Parkway, Mountain View, CA",
"1 Infinite Loop, Cupertino, CA",
],
},
});
if (error) throw error;
for (const item of geocoding.results) {
if (item.status === "ok") console.log(item.original, item.lat, item.lng, item.precision);
else console.log(item.original, item.status); // "ambiguous" (see item.candidates) | "not_found"
}
console.log(geocoding.priceMicros); // 5000 per distinct addressAPI Keys
Create Key
Generate a new API key. The raw key (prefixed with acc_) is returned only once in this response -- store it securely.
const { data: key, error } = await r4m.createApiKey({
body: { name: "Production Server" },
});
if (error) throw error;
console.log(key.id); // "550e8400-..."
console.log(key.key); // "acc_a1b2c3d4e5f6..." (store this — shown once)
console.log(key.prefix); // "acc_a1b2c3"List Keys
List all API keys. Raw keys are never returned -- only the prefix is shown.
const { data: keys, error } = await r4m.listApiKeys();
if (error) throw error;
for (const key of keys) {
console.log(key.name, key.prefix, key.lastUsedAt);
}Revoke Key
Revoke an API key (soft delete). The key stops working immediately.
const { data: revoked, error } = await r4m.revokeApiKey({
path: { id: "550e8400-..." },
});
if (error) throw error;
console.log(revoked.id); // the key that was revokedUsage & Account
Get Usage
Get the current prepaid balance (micro-dollars) and recent activity.
const { data: usage, error } = await r4m.getUsage();
if (error) throw error;
console.log(usage.balanceMicros); // 48200000 = $48.20
console.log(usage.ledger[0].amountMicros); // -10000 = -$0.01
console.log(usage.ledger[0].reason); // "usage"Get Account
Get current user account information and plan details.
const { data: account, error } = await r4m.getAccount();
if (error) throw error;
console.log(account.email); // "dev@example.com"
console.log(account.planTier); // "pro"
console.log(account.balanceMicros); // 48200000 = $48.20
console.log(account.plan.maxAddressesPerRequest); // 100Error Handling
All API errors follow a consistent format with a machine-readable code and a human-readable message. Common error codes: VALIDATION_ERROR, UNAUTHORIZED, INSUFFICIENT_FUNDS, NOT_FOUND, RATE_LIMITED.
// Calls return an error, they never reject. TypeScript will not let you read
// `data` until you have ruled `error` out.
const { data, error } = await r4m.createOptimization({
body: { addresses: [] },
});
if (error) {
console.error(error.code); // "VALIDATION_ERROR"
console.error(error.message); // "Too small: expected array to have >=3 items"
console.error(error.status); // 400; undefined if no response arrived
} else {
console.log(data.id);
}Waiting for the Result
Optimizations are processed asynchronously — typical processing takes 2-60 seconds depending on complexity. If you have a public endpoint, webhooks save you from waiting at all. Otherwise the SDK can wait for you.
Let the SDK wait
One call, and you get the finished plan. Use waitForRoutes instead when the id came from somewhere else — your own queue, a database row, an earlier process.
// Submit and wait in one call
const { data: optimization, error } = await r4m.createOptimizationAndWaitForRoutes({
body: {
addresses: [
{ address: "Depot", lat: 40.7128, lng: -74.006, isDepot: true },
{ address: "Stop 1", lat: 40.758, lng: -73.9855 },
{ address: "Stop 2", lat: 40.7484, lng: -73.9857 },
],
},
timeoutMs: 120_000, // optional; defaults to 5 minutes
});
if (error) throw error; // refused, failed, timed out or still busy; see Error Handling
console.log(optimization.result);
// Or wait on an id you already have
const finished = await r4m.waitForRoutes("550e8400-e29b-41d4-a716-446655440000");
if (finished.error) throw finished.error;
console.log(finished.data.result);When the API says busy
A 429 means busy, whichever limit said so: the per-minute rate limit (RATE_LIMITED) or your plan's optimizations-in-flight allowance (CONCURRENCY_LIMIT_EXCEEDED, Free 1, Pro 2). The helpers above wait it out for you — honouring Retry-After and polling one of the optimizations holding a slot — for up to two minutes, then fail. Pass onBusy to decide per event (it is synchronous: return "wait" or "fail", never sleep), use submitOptimization when a webhook will collect the plan, and waitForActiveOptimizations to hold until nothing of yours is running.
// Submit only — a webhook will bring the plan — but wait out "busy".
const { data: receipt, error } = await r4m.submitOptimization({
body: { addresses, webhookUrl: "https://example.com/hooks/r4m" },
busyTimeoutMs: 60_000, // optional; defaults to 2 minutes
onBusy: (event) => {
// event.reason: "RATE_LIMITED" | "CONCURRENCY_LIMIT_EXCEEDED"
// event.activeOptimizationIds: what is holding the slots
// event.timedOut: busyTimeoutMs has passed — only "fail" counts now
console.log(`busy (${event.reason}), retry in ${event.retryAfterMs}ms`);
return event.timedOut ? "fail" : "wait";
},
});
if (error) throw error;
console.log(receipt.id);
// Or hold until nothing of yours is running, then submit a batch.
const idle = await r4m.waitForActiveOptimizations({
onWait: ({ active, elapsedMs }) =>
console.log(`${active} still running after ${elapsedMs}ms`),
});
if (idle.error) throw idle.error;Or poll it yourself
Right when submission and collection happen in different places. Two things such a loop has to get right. Back off between attempts rather than hammering a fixed interval — the examples below go 2s, 4s, then settle at 5s. And when a poll comes back 429, wait for the number of seconds in the Retry-After header instead of retrying immediately. For production use, also treat a network error or a 5xx as transient and keep waiting: the optimization is running regardless of whether you are connected, so a blip should not abandon a result you have already paid for.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForResult(id: string, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
let delay = 2000;
while (Date.now() < deadline) {
const { data: optimization, error } = await r4m.getOptimization({ path: { id } });
if (error) {
// Throttled — wait exactly as long as we asked, and don't let this
// attempt widen the backoff.
if (error.status === 429) {
await sleep(error.retryAfterMs ?? 5000);
continue;
}
// A 401 or 404 cannot change; a network error or 5xx can.
if (error.status !== undefined && error.status < 500) throw error;
} else if (optimization.status === "completed") {
return optimization.result;
} else if (optimization.status === "failed") {
throw new Error(optimization.errorMessage ?? "Optimization failed");
}
await sleep(delay);
delay = Math.min(delay * 2, 5000); // 2s, 4s, then every 5s
}
throw new Error("Timed out waiting for the optimization");
}Try the API live
Send requests directly from your browser with the interactive API reference powered by Scalar.
Open Interactive API Reference