Documentation
Rydmate API reference.
Everything you need to authenticate, read and write the operation, and hear about what changes. The OpenAPI contract is the source of truth for every field; this page explains how the pieces fit. Looking for how to use the dashboard instead? That is the product guide.
Quick start
Create an account, open Developers in the dashboard and create a test key: a name, and whether it is for the sandbox or the live business. The first test key provisions your sandbox, a copy of your business with simulated vehicles, drivers, routes and a departure. Then:
curl https://api.rydmate.com/api/v1/vehicles \
-H "Authorization: Bearer ryd_test_your_key"Every success body is { "data": ..., "meta": ... }. Every error body is { "error": { "code", "message", "request_id" } }. Money is always integer kobo in fields ending _kobo; timestamps are RFC 3339 UTC.
Authentication
Send the key as a bearer token on every request:
Authorization: Bearer ryd_live_0k3f…A key that is unknown, revoked or expired answers 401 with no further detail. A record that belongs to another business answers 404, never 403, so its existence is not confirmed.
Keys never reach the session-only surface: the business itself, its people, its plan, its website, its keys and its webhooks are managed by a signed-in owner.
Environments and the sandbox
There are two environments, told apart by the key prefix. ryd_test_ keys act on your sandbox and only your sandbox. ryd_live_ keys act on your real business and need a Rydmate API plan, or a Rydmate Cloud plan that includes the integration API (Scale and Enterprise).
The sandbox is a full business of its own, on the Scale plan with no trial clock. Nothing in it is billed, nothing in it reaches production, and you can see it on the dashboard by switching business. Webhooks registered as test receive events from the sandbox; live endpoints receive those of the real business.
API keys
A key has a name and an environment, and it can do everything the API can do for your business. The secret is shown once when the key is created or rotated; Rydmate stores a hash and cannot show it again. The dashboard lists a hint (ryd_live_…k7q2), when it was last used, and whether it is active.
If you ever need a narrower key, for a partner who should only read, pass scopes when creating one over the API: vehicles:read, shipments:write and so on, one per resource. Most integrations never will.
Rotate a key to mint a replacement with the same name, environment and scopes; the old secret stops working in the same transaction. Revoke a key to end it: the next request with it is a 401. Every key action is recorded in the audit trail of the business.
Rate limits
Each key has an allowance per minute, with a burst so a short spike is not refused. Every response says where you stand:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587
X-RateLimit-Reset: 1758200400Over the limit, the response is 429 with a Retry-After header. Sandbox keys have a lower allowance than live keys; live allowances are set by your plan, and Scale and Enterprise plans can raise them.
Pagination
Small collections (branches, vehicles, drivers, routes, users) use page numbers: ?page=2&page_size=50, with meta.page, meta.page_size, meta.total and meta.total_pages on the response.
High-volume feeds (payments, fare transactions, tracking history) use cursors: pass meta.next_cursor back as ?cursor= until meta.has_more is false. Cursors are opaque and stay valid while you page.
Errors
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request could not be processed.",
"request_id": "8f1c…",
"details": [{ "field": "seat_capacity", "issue": "must be between 1 and 120" }]
}
}400 malformed request, 401 not authenticated, 403 not permitted, 404 not found or not yours, 409 a conflict (a seat already sold, an idempotency key reused, a state that cannot move that way), 422 validation, 429 rate limited, 500 our fault. Quote the request_id when you write to support.
Idempotency
Any write may carry an Idempotency-Key header of your choosing, up to 255 characters. If the same key is sent again with the same method, path and body within 24 hours, the stored response is replayed with an Idempotent-Replayed: true header, and nothing is created twice. The same key with a different request answers 409.
curl -X POST https://api.rydmate.com/api/v1/bookings \
-H "Authorization: Bearer ryd_live_…" \
-H "Idempotency-Key: booking-7f3a" \
-H "Content-Type: application/json" \
-d '{ "trip_id": "…", "seat_number": "3A", "passenger": { "full_name": "Ngozi Okafor" }, "channel": "ONLINE" }'Use it on every ticket, shipment and payment you create. A retry over a flaky connection then never sells a seat twice.
Resources
| Endpoint | Scope, if you narrow a key | What it is |
|---|---|---|
| GET /api/v1/branches | branches:read | Branches of the business. |
| GET, POST /api/v1/vehicles | vehicles:read, vehicles:write | Vehicles, with PATCH and DELETE on /vehicles/{id}. |
| GET, POST /api/v1/drivers | drivers:read, drivers:write | Drivers; POST /drivers/{id}/assign mounts one on a vehicle. |
| GET, POST /api/v1/routes | routes:read, routes:write | Routes and their fares (PUT /routes/{id}/fare). |
| GET, POST /api/v1/trips | trips:read, trips:write | Departures. POST /trips/{id}/transition moves them along; /trips/{id}/seats is the seat map. |
| GET, POST /api/v1/bookings | tickets:read, tickets:write | Tickets. A booking is a sold seat; cancel it with a transition. |
| GET, POST /api/v1/orders | shipments:read, shipments:write | Logistics orders. POST /orders/{id}/shipments, then /shipments/{id}/dispatch, /transition, /deliver. |
| GET /api/v1/tracking/vehicles | tracking:read | Live positions; /tracking/vehicles/{id}/history for the trail. |
| POST /api/v1/telemetry/positions | tracking:write | Post GPS fixes for registered devices. |
| GET /api/v1/payments, /fares | transactions:read | What was collected, and the receipts for it. |
| GET /api/v1/reports/{report} | reports:read | The operational reports as data. |
| GET /api/v1/public/track/{code} | none | Public shipment tracking by waybill or tracking code. |
Every field, parameter and response is in the OpenAPI contract below. The shapes are the same ones the Rydmate dashboard uses.
GPS telemetry
Any device or gateway that can post JSON can feed Rydmate. Register the device once with the id your feed will use, mount it on a vehicle, then post positions, one or many per request:
POST /api/v1/telemetry/devices
{ "external_id": "unit-77", "name": "Unit 77" }
POST /api/v1/tracking/devices/{id}/assign
{ "vehicle_id": "…" }
POST /api/v1/telemetry/positions
[
{ "device_id": "unit-77", "timestamp": "2026-09-18T12:00:00Z",
"latitude": 9.0605, "longitude": 7.4620, "speed_kmh": 41.5, "heading": 180 }
]The response says how many fixes were stored and which were rejected and why. A device unknown to your business is rejected by index; the rest are stored. Implausible jumps are kept as evidence but never move the map.
Webhooks
Register an https endpoint and the events it wants. Each delivery is a POST with a JSON envelope:
{
"id": "01a0…",
"type": "shipment.created",
"environment": "live",
"created_at": "2026-09-18T12:00:00Z",
"data": { "shipment_id": "…", "order_id": "…", "waybill_number": "…", "status": "CREATED" }
}| trip.created, trip.started, trip.completed, trip.cancelled | A departure was scheduled or changed state. |
| ticket.created, ticket.cancelled | A seat was sold or released. |
| shipment.created, shipment.assigned, shipment.in_transit, shipment.delivered | A shipment moved along its journey. |
| payment.recorded, payment.confirmed | Money was recorded by staff, or confirmed by the provider. |
| vehicle.location.updated | A tracked vehicle reported a new position. |
Deliveries are retried up to six times over roughly a day (after 1, 5, 30 minutes, then 2 and 12 hours). Answer with any 2xx within ten seconds. The dashboard shows every delivery, its response and lets you retry one by hand.
Verifying a delivery
Every delivery carries Rydmate-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 with the secret of your endpoint over <t>.<raw body>. Refuse a timestamp older than five minutes, and compare in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Payloads carry identifiers, statuses and amounts, never the contact details of a passenger or customer. Read the resource if you need more.
OpenAPI contract
The full contract is served by the API itself and is the source of truth for every field. Generate a client from it in the language you use.
curl https://api.rydmate.com/api/v1/openapi.yamlReady to build? Book a demo; every business gets a test key and a sandbox with its account.