---
title: RoamWorks Partner API
version: 1.0.0
updated: 2026-11-04
audience: developers and AI agents
auth: API ID and API secret, HMAC-SHA256
canonical: https://roamworks.io/docs/api.md
openapi: https://roamworks.io/docs/openapi/v1.json
---

# RoamWorks Partner API

Use this file as the complete machine-readable companion to the hosted reference. It is public; never put an API secret in it.

## Environments

| Environment | API base | Dashboard |
| --- | --- | --- |
| Production | `https://roamworks.tech/v1` | `https://partners.roamworks.io/partners` |
| Sandbox | `https://sandbox.roamworks.tech/v1` | `https://sandbox.partners.roamworks.io/partners` |

Production wallet funding is enabled through Stripe Checkout. Sandbox funding is disabled.

Keys and data are environment-specific. A key from one environment never works in the other. `roamworks.tech` is API-only; documentation lives at `https://roamworks.io/docs`.

## Authentication

Every request carries an API ID and an API secret. The API ID is public. The secret is shown once and never returned again.

Headers:

- `x-partner-key-id`: the API ID
- `x-partner-timestamp`: Unix seconds, within 300 seconds of the server
- `x-partner-nonce`: a unique value, single use
- `x-partner-signature`: lowercase HMAC-SHA256 hex

Canonical string, LF separated:

```
RW-HMAC-SHA256-V1
<apiId>
<sandbox|live>
<timestamp>
<nonce>
<METHOD>
<authority>
<path and query>
<idempotency-key or empty>
<SHA-256 of the raw body>
```

The signature is HMAC-SHA256(secret, canonical string). Clock drift over five minutes is rejected. A replayed nonce is rejected.

## IP allowlist

Each key may have an IPv4 or IPv6 address or CIDR allowlist. A request from outside it returns `403 ip_not_allowed`. The list is configured by the principal when the key is issued. Dynamic agent egress must be allowlisted or the key must have no allowlist.

## Limits

Limits are per API ID, with a five-times account aggregate. A daily quota applies per call class. Responses include `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `RateLimit-Policy` and, on 429, `Retry-After`.

| Class | Sustained | Daily |
|---|---:|---:|
| Capabilities, manifest, usage | 120/min | 20,000 |
| Catalogue and offer reads | 120/min | 20,000 |
| Quote creation | 60/min | 2,000 |
| Order creation | 30/min | 1,000 |
| Order reads | 120/min | 20,000 |
| Redemption | 20/min | 1,000 |
| Wallet funding | 5/min | 20 |
| Agent and key management | 10/min | 50 |

`429 rate_limited` is a burst or sustained limit. `429 quota_exceeded` is a daily quota. Neither creates a quote, order or funding operation. `GET /v1/usage` reports the current state.

Bulk limits: 200 lines and 5,000 units per quote. Agent defaults: 50 lines and 500 units per order unless the principal raises them.

## Endpoints

### GET /v1/capabilities

Public. Returns API version, auth modes, limits, capabilities and documentation URLs.

### GET /v1/agent-manifest.json

Public. Machine-readable card for agents: auth, scopes, limits, quote minutes, bulk limits and URLs.

### GET /v1/catalog

Authenticated scope `catalog:read`. Cursor pagination and filters. Do not assume a page contains every offer.

### POST /v1/quotes

Authenticated scope `quotes:write`. Body: `{ currency, items: [{ offerId, quantity }] }`. Returns a quote that lasts 15 minutes. Up to 200 lines and 5,000 units.

### POST /v1/orders

Authenticated scope `orders:write`. Requires `Idempotency-Key`. Either:

```json
{ "reference": "booking-123", "quoteId": "..." }
```

or one-call buying:

```json
{
  "reference": "booking-123",
  "currency": "USD",
  "expectedTotalMinor": 1234,
  "items": [{ "offerId": "...", "quantity": 2 }]
}
```

Returns 201 with a purchase id and status. `processing` means supplier fulfilment is asynchronous. Poll `GET /v1/orders/{id}`. If `expectedTotalMinor` differs, returns `409 price_changed` with the new total and does not charge.

### GET /v1/orders

Authenticated scope `orders:read`. List your purchase operations.

### GET /v1/orders/{id}

Authenticated scope `orders:read`. Returns purchase, child orders, status and a `delivery` object with `status`, `startedAt`, `completedAt` and `durationMs`. Statuses: `processing`, `partially_fulfilled`, `completed`, `failed`, `cancelled`.

### GET /v1/orders/{id}/redeem

Authenticated scope `esims:redeem`. Returns the neutral LPA and QR material for issued eSIMs.

### POST /v1/orders/{id}/redeem

Authenticated scope `esims:redeem`. Creates a redemption batch.

### POST /v1/agents

Principal only, scope `agents:manage`. Body: name, purpose, scopes, per-order limit, daily limit, rolling limit, approval threshold and optional expiry. Returns an agent id.

### PATCH /v1/agents/{id}

Principal only. Updates scopes, limits or status. An agent cannot edit itself.

### DELETE /v1/agents/{id}

Principal only. Revokes the agent and all its keys.

### POST /v1/agents/{id}/keys

Principal only. Body: optional name and `ipAllowlist` array. Returns `apiId` and `apiSecret` once. Store the secret immediately.

### POST /v1/agents/{id}/keys/{keyId}/rotate

Principal only. Returns a new API ID and secret. The old secret expires after 24 hours.

### GET /v1/agents/me

Agent only. Returns its id, name, scopes, environment and partner id. Never returns a secret.

### GET /v1/delivery

Authenticated scope `orders:read`. Returns measured eSIM delivery latency for completed orders in the last 30 days: `sampleSize`, `medianMs`, `p90Ms`, `under10sPercent`, `fastestMs` and `slowestMs`. The clock starts at payment, or at order creation for wallet orders, and stops when the eSIM is issued and attached to a fulfilled order. A small sample is reported as a small sample, not rounded up into a marketing claim.

### GET /v1/usage

Authenticated. Returns current period, call counts, errors, limits and wallet balances.

### GET /v1/activity

Authenticated. Returns the last 50 calls in the last seven days and current error groups. An agent sees only its own calls.

### GET /v1/wallet

Authenticated scope `wallet:read`. Returns USD, EUR and GBP posted, held and available balances.

### POST /v1/wallet/funding

Authenticated scope `wallet:fund`. Requires `Idempotency-Key`. Body: `{ amountMinor, currency }`. Returns a Stripe Checkout URL and pending operation. No credit is issued until the signed Stripe webhook confirms payment.

## Payment states

- `402 payment_required`: insufficient wallet credit; read `funding_url`.
- `approval_required`: the order is above the agent or principal threshold; a human must approve.
- `403 ip_not_allowed`: fix the allowlist.
- `429`: wait `Retry-After`.
- `409 price_changed`: get a fresh quote or retry the one-call order.

The agent never receives card data and never calls Stripe directly. A principal may authorise a capped off-session top-up mandate; if Stripe requires action, the API returns an action URL and does not claim success.

## Errors

Every error has this shape:

```json
{ "error": { "code": "rate_limited", "message": "...", "requestId": "...", "retryAfter": 45 } }
```

The `requestId` is also returned in `X-Request-Id` and can be used by support. Do not retry non-idempotent calls without the same `Idempotency-Key`.

## Neutrality

Products are RoamWorks offers. Supplier names, source stores, supplier SKUs, costs and portals are never exposed. Public names are sanitised. If a product cannot be delivered white-label, it is not sold.

## Sandbox

Use a sandbox API ID and secret. Sandbox credentials cannot call live. A sandbox flow needs no browser: create a test wallet through the test-mode funding path, search the catalogue, place a one-call order, poll the order and read usage.