SDK Reference

Auto-generated, type-safe SDKs for TypeScript and Go. Complete method reference for every API operation.

Installation & Setup

Install

npm install @r4mai/sdk

Initialize

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 removed

Geocoding

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 address

API 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 revoked

Usage & 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); // 100

Error 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