Platform

Public API and webhooks

A versioned REST API for embedding Renaro in your own booking site — with idempotency, cursor pagination, and replay-protected signed webhooks that most dispatch vendors simply do not offer.

11 min read · updated

Authentication and versioning

Server-to-server integrations authenticate with a secret API key in the X-API-Key header and call the versioned surface at /api/v1. Keys are organization-scoped, stored hashed, and revocable individually — rotate without downtime by overlapping two keys. Browser code uses a separate publishable key and never sees a secret key.

The API is date-versioned. Pin a contract with the Renaro-Version header; omit it and you resolve to the oldest supported version, so a live integration never changes behaviour without opting in. That is the discipline Stripe made standard — and something no ground-transport dispatch vendor we surveyed offers.

Authenticated request
curl https://api.renaroapp.com/api/v1/bookings \
  -H "X-API-Key: rsk_live_..." \
  -H "Renaro-Version: 2026-07-16"

Quote, then book at a locked price

Create a quote and its pricing snapshot is locked until expiresAt — a short, self-describing window, not an open-ended promise. Bind it into a booking by passing the quote id, and the quoted price is authoritative. The lifecycle is explicit in the response: status, acceptReady, and expiresInSeconds, so your client always knows whether a quote can still be used.

POST /api/v1/quotes → response
{
  "data": {
    "id": "9f0e8400-...",
    "expiresAt": "2026-07-16T14:45:00Z",
    "status": "open",
    "acceptReady": true,
    "expiresInSeconds": 900,
    "quote": { "currency": "GBP", "estimatedFareCents": 4620 }
  },
  "meta": { "requestId": "req_...", "timestamp": "2026-07-16T14:30:00Z" }
}

Idempotency, pagination, and errors

Every mutation accepts an Idempotency-Key. Retry the same request with the same key and you get the original response back; reuse a key with a different body and you get a clear IDEMPOTENCY_KEY_REUSED error rather than a duplicate booking. Lists are cursor-paginated. Errors are structured — a code, a message, and field-level details — and every response carries a request id you can quote to support.

  • Idempotency-Key on every write, with replay and reuse detection.
  • Cursor pagination on every list — no offset drift on a moving dataset.
  • Per-key rate limits with X-RateLimit-* and Retry-After headers.

Signed webhooks with replay protection

Subscribe to booking, dispatch, driver, passenger, and payment events. Each delivery is signed with HMAC-SHA256 over the timestamp and body — the same construction as Stripe — and carries a unique event id so a redelivery is harmless. Verify the signature and enforce a timestamp tolerance: comparing the signature alone lets a captured payload replay forever; a tolerance window closes it. Deliveries only reach public HTTPS endpoints, and failures retry with exponential backoff.

  • X-Renaro-Signature: t=<timestamp>,v1=<hmac-sha256>.
  • booking.created, booking.assigned, booking.completed, booking.cancelled.
  • dispatch.*, driver.*, passenger.*, payment.captured, payment.refunded.
Verify a delivery
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const { t, v1 } = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=")),
  );
  if (Math.abs(Date.now() - Date.parse(t)) > toleranceSec * 1000) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

Design rules worth copying

Everything retryable is idempotent. Money amounts are integer cents. Timestamps are timezone-aware ISO-8601. Free-form data you send — booking metadata, pricing breakdowns — comes back exactly as you stored it, never silently renamed. Requests carry ids end to end, so a support conversation can reference the exact call.