# Authentication (/docs/authentication)
The OpenRelay API authenticates requests with an **API key**. Each key is bound
to a single organization and carries a set of scopes. Send it as a bearer token
on every request:
```bash
curl https://api.openrelay.inc/v1/me \
-H "Authorization: Bearer vl_your_api_key"
```
API keys are prefixed with `vl_`. Treat them like passwords.
Never commit API keys to source control, embed them in client-side code, or
share them in screenshots. Use environment variables or a secrets manager. If
a key leaks, revoke it immediately and issue a new one.
## Create an API key [#create-an-api-key]
### From the dashboard [#from-the-dashboard]
Go to **Settings → API Keys** in the [dashboard](https://app.openrelay.inc),
create a key, and copy it. The plaintext value is shown **once** — store it
somewhere safe.
### Or via the API [#or-via-the-api]
If you already have a key (or a session), you can mint more. The plaintext key
is returned **once** in the response and never again:
```bash
curl -X POST https://api.openrelay.inc/v1/orgs/{orgId}/api-keys/create \
-H "Authorization: Bearer vl_existing_key" \
-H "Content-Type: application/json" \
-d '{ "name": "ci-pipeline" }'
```
```json
{
"id": "key_…",
"name": "ci-pipeline",
"key": "vl_…", // plaintext — shown once
"keyPrefix": "vl_abc12"
}
```
See [Create an API key](/docs/api-keys/createOrgApiKey) in the reference.
## Using your key [#using-your-key]
Pass the key in the `Authorization` header as `Bearer vl_…` on every request.
Requests without a valid key receive `401 Unauthorized`; a key that lacks the
required scope (or targets another org) receives `403 Forbidden`.
```bash title="Set it once in your shell"
export OPENRELAY_API_KEY="vl_your_api_key"
curl https://api.openrelay.inc/v1/orgs/$ORG_ID/clusters \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
## Scopes [#scopes]
Keys carry scopes such as `clusters:read`, `vms:write`, and `billing:read`.
Grant a key only the scopes it needs — for example, a monitoring job that only
reads usage should get read scopes, not write. A request that exceeds a key's
scopes returns `403`.
## Rotating and revoking [#rotating-and-revoking]
* **Rotate** by creating a new key, deploying it, then revoking the old one.
* **Revoke** instantly with
[`DELETE /v1/orgs/{orgId}/api-keys/{id}`](/docs/api-keys/revokeOrgApiKey) or
from the dashboard. Revocation takes effect within seconds.
This reference documents **API key** auth (`vl_…`), which is what you'll use
for automation and SDKs. The dashboard itself uses short-lived user session
tokens — you don't need those to build on the API.
# Errors (/docs/errors)
The API uses conventional HTTP status codes. `2xx` means success. `4xx` means
the request was rejected (and usually shouldn't be retried unchanged). `5xx`
means something went wrong on our side.
## Error body [#error-body]
Every error response has a JSON body with a human-readable `error` message and
an optional machine-readable `code`:
```json
{
"error": "cluster not found",
"code": "not_found"
}
```
Always branch on the HTTP **status code** first; treat `code` as a stable hint
and `error` as a message for logs and humans.
## Status codes [#status-codes]
| Status | Meaning | What to do |
| ------ | ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `200` | OK | Success. |
| `400` | Bad Request | Malformed JSON or invalid parameters. Fix the request. |
| `401` | Unauthorized | Missing, invalid, or expired API key. Check the `Authorization` header. |
| `403` | Forbidden | The key is valid but lacks the scope, or targets another org. |
| `404` | Not Found | The resource doesn't exist (or isn't visible to your org). |
| `409` | Conflict | The request conflicts with current state (e.g. a name clash, or an action invalid for the resource's status). |
| `422` | Unprocessable | The request is well-formed but semantically invalid. |
| `429` | Too Many Requests | You're being rate limited. Back off and retry. |
| `5xx` | Server Error | A transient problem on our end. Retry with backoff. |
## Retrying [#retrying]
Retry only **idempotent** requests (`GET`, and creates you can safely repeat)
and only on `429` and `5xx`. Use **exponential backoff with jitter**, and
respect the `Retry-After` header on `429` responses when present.
```bash
# Example: retry a GET up to 5 times with backoff
for i in 1 2 4 8 16; do
if curl -fsS https://api.openrelay.inc/v1/vms/$VM_ID \
-H "Authorization: Bearer $OPENRELAY_API_KEY"; then
break
fi
sleep "$i"
done
```
## Diagnosing failures [#diagnosing-failures]
For resources that can land in a failed or stuck state (VMs, clusters), the
resource body carries a `statusReason` field with a human-readable cause — read
it before retrying a create.
# Introduction (/docs)
The **OpenRelay API** is the programmatic interface to the OpenRelay GPU cloud.
Everything you can do in the [dashboard](https://app.openrelay.inc) — launch GPU
VMs, run autoscaling inference clusters, manage SSH keys, top up your balance,
subscribe to events — you can do over HTTPS with a single API key.
It is a predictable, resource-oriented REST API: JSON request and response
bodies, standard HTTP verbs and status codes, and bearer-token authentication.
## Base URL [#base-url]
All requests go to:
```
https://api.openrelay.inc
```
Every endpoint is versioned under `/v1`. For example:
```bash
curl https://api.openrelay.inc/v1/me \
-H "Authorization: Bearer vl_your_api_key"
```
Most resources live inside an **organization**. Endpoints that operate on a
collection are nested under `/v1/orgs/{orgId}/…`, and your API key is bound to
a single organization — so the `orgId` in the path must match the key's org.
## Conventions [#conventions]
* **Format** — Requests and responses are `application/json`. Send
`Content-Type: application/json` on any request with a body.
* **Authentication** — Every request carries `Authorization: Bearer vl_…`.
See [Authentication](/docs/authentication).
* **Timestamps** — ISO 8601 / RFC 3339 strings in UTC (e.g. `2026-06-10T12:00:00Z`).
* **Money** — Monetary amounts are integer **cents** unless noted otherwise.
* **IDs** — Opaque strings; don't parse them.
* **Errors** — A non-2xx status with a JSON `{ "error": "…" }` body.
See [Errors](/docs/errors).
* **Pagination** — List endpoints are cursor-paginated.
See [Pagination](/docs/pagination).
## Get started [#get-started]
## Built for tools and LLMs [#built-for-tools-and-llms]
These docs are machine-readable out of the box:
* The full spec is served at [`/openapi.json`](/openapi.json) — drop it into
Postman, an SDK generator, or your editor.
* Every page is available as Markdown — use the **Copy Markdown** button, or
append `.md`-style content routes for any page.
* [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) expose the
whole site for AI assistants.
# Pagination (/docs/pagination)
List endpoints that can return many items are **cursor-paginated**. You request
a page, and the response tells you how to fetch the next one.
## Request [#request]
Pass two optional query parameters:
* `limit` — maximum number of items to return for this page.
* `cursor` — an opaque pointer to the next page, taken from the previous response.
```bash
curl "https://api.openrelay.inc/v1/orgs/$ORG_ID/clusters?limit=50" \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
## Response [#response]
A page returns an `items` array and, when more results exist, a `nextCursor`:
```json
{
"items": [
{ "id": "cl_…", "name": "prod-llama", "status": "running" }
],
"nextCursor": "eyJpZCI6…"
}
```
When `nextCursor` is **absent (or empty)**, you've reached the last page.
## Iterating all pages [#iterating-all-pages]
Pass the previous `nextCursor` as the next request's `cursor` until it's gone:
```bash
cursor=""
while :; do
resp=$(curl -fsS \
"https://api.openrelay.inc/v1/orgs/$ORG_ID/clusters?limit=100&cursor=$cursor" \
-H "Authorization: Bearer $OPENRELAY_API_KEY")
echo "$resp" | jq -c '.items[]'
cursor=$(echo "$resp" | jq -r '.nextCursor // empty')
[ -z "$cursor" ] && break
done
```
```ts title="TypeScript"
async function* listClusters(orgId: string) {
let cursor: string | undefined;
do {
const url = new URL(`https://api.openrelay.inc/v1/orgs/${orgId}/clusters`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.OPENRELAY_API_KEY}` },
});
const page = await res.json();
yield* page.items;
cursor = page.nextCursor || undefined;
} while (cursor);
}
```
A cursor encodes our position in the result set — don't parse, modify, or
construct one. Always use the exact `nextCursor` value from the previous
response.
# Quickstart (/docs/quickstart)
This guide takes you from zero to a running GPU VM using nothing but `curl`.
### Get your credentials [#get-your-credentials]
Create an API key (see [Authentication](/docs/authentication)) and grab your
organization id. Both are visible in the [dashboard](https://app.openrelay.inc),
or call `/v1/me`:
```bash
export OPENRELAY_API_KEY="vl_your_api_key"
curl https://api.openrelay.inc/v1/me \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
```json
{
"user": { "id": "usr_…", "email": "you@example.com" },
"memberships": [
{ "organizationId": "org_…", "role": "owner", "name": "Acme Inc" }
]
}
```
```bash
export ORG_ID="org_…"
```
### Browse the catalog [#browse-the-catalog]
See what GPUs are available and what they cost:
```bash
curl https://api.openrelay.inc/v1/gpu-availability \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
curl https://api.openrelay.inc/v1/pricing \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
Note a `gpuModelId` with available capacity — you'll use it next.
### Launch a GPU VM [#launch-a-gpu-vm]
Create a VM. At minimum it needs a `name`; specify a `gpuModelId` and an
`imageUrl` to pick hardware and the container image to run.
```bash
curl -X POST https://api.openrelay.inc/v1/orgs/$ORG_ID/vms/create \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-first-vm",
"gpuModelId": "rtx-4090",
"gpuCount": 1,
"imageUrl": "ghcr.io/my-org/my-image:latest",
"diskSizeGb": 100,
"envVars": { "MODEL": "llama-3" }
}'
```
The response includes the new VM's `id` and initial `status`.
### Poll until it's running [#poll-until-its-running]
Provisioning takes a little while. Poll the VM until `status` becomes `running`:
```bash
curl https://api.openrelay.inc/v1/vms/$VM_ID \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
```json
{
"id": "vm_…",
"name": "my-first-vm",
"status": "running",
"endpointUrl": "https://my-first-vm-….run.openrelay.inc",
"pricePerHourCents": 59
}
```
Prefer [webhooks](/docs/webhooks/overview) over polling — subscribe to `vm.running`
and get a callback the moment your VM is live.
### Connect [#connect]
Attach an [SSH key](/docs/ssh-keys/overview) to reach the box, or hit the
`endpointUrl` your container exposes. To stop billing, stop or terminate the VM:
```bash
curl -X POST https://api.openrelay.inc/v1/vms/$VM_ID/terminate \
-H "Authorization: Bearer $OPENRELAY_API_KEY"
```
## Next steps [#next-steps]
* Run an autoscaling endpoint instead of a single box → [Clusters](/docs/clusters/overview)
* Automate top-ups and avoid interruptions → [Billing](/docs/billing/overview)
* React to lifecycle events → [Webhooks](/docs/webhooks/overview)
* Explore every endpoint interactively → [API Reference](/docs/vms/overview)
# Create the caller's profile + first org (idempotent). Supabase-JWT only. (/docs/account/bootstrap)
`POST /v1/onboarding/bootstrap`
Authentication: Bearer API key (`vl_…`)
## Request body
`application/json`:
- `orgName` string
## Responses
- `200` — Bootstrapped (or already existed)
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Current user profile + org memberships (/docs/account/getMe)
`GET /v1/me`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — The caller's profile + orgs
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/account/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Account [#account]
The signed-in user: profile, memberships, and first-time onboarding.
# Update the caller's profile (/docs/account/updateProfile)
`PATCH /v1/me/profile`
Authentication: Bearer API key (`vl_…`)
## Request body
`application/json`:
- `fullName` string
- `avatarUrl` string
- `defaultOrganizationId` string
## Responses
- `200` — Updated
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# AI proxy connection info + proxy-scoped keys (/docs/ai-proxy/getAiProxy)
`GET /v1/orgs/{orgId}/ai/proxy`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# AI proxy usage (daily + totals) (/docs/ai-proxy/getAiProxyUsage)
`GET /v1/orgs/{orgId}/ai/proxy-usage`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `days` (query) integer
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/ai-proxy/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## AI Proxy [#ai-proxy]
OpenAI-compatible proxy connection details and usage.
# Create an API key (returns the plaintext once) (/docs/api-keys/createOrgApiKey)
`POST /v1/orgs/{orgId}/api-keys/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `scopes` array
## Responses
- `200` — Created
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's API keys (no secrets) (/docs/api-keys/listOrgApiKeys)
`GET /v1/orgs/{orgId}/api-keys`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — API keys
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/api-keys/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## API Keys [#api-keys]
Programmatic access keys (vl\_…) used to authenticate with this API.
# Revoke an API key (/docs/api-keys/revokeOrgApiKey)
`DELETE /v1/orgs/{orgId}/api-keys/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `204` — Revoked
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Charge the saved card to top up prepaid balance (/docs/billing/createBillingDeposit)
`POST /v1/orgs/{orgId}/billing/deposit`
Charges the org's default saved card off-session. The balance credit is applied by the payment_intent.succeeded webhook (the authoritative money signal), not this response — poll the balance after success.
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `amountCents` integer (required) — Amount to charge the saved card, in cents (bounds $10–$500).
- `idempotencyKey` string — Client-generated id for this logical deposit; reused verbatim on retry so a network/client retry can't double-charge. Optional — the server generates one if absent (then retries won't dedupe).
## Responses
- `200` — Charge outcome (status drives the dashboard's next step)
- `400` —
- `401` —
- `403` —
- `503` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Start saving a card (Stripe SetupIntent for inline Elements) (/docs/billing/createBillingSetupIntent)
`POST /v1/orgs/{orgId}/billing/setup-intent`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — SetupIntent client secret + publishable key
- `401` —
- `403` —
- `503` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Remove a saved card (/docs/billing/deleteBillingPaymentMethod)
`DELETE /v1/orgs/{orgId}/billing/payment-methods/{paymentMethodId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `paymentMethodId` (path, required) string
## Responses
- `204` — Removed
- `401` —
- `403` —
- `503` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Read the org's auto-recharge policy (/docs/billing/getAutoRecharge)
`GET /v1/orgs/{orgId}/billing/auto-recharge`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Current settings (defaults when never set)
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Org balance + recent transactions (/docs/billing/getBalance)
`GET /v1/orgs/{orgId}/billing/balance`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Balance
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List saved cards (brand/last4/exp only) (/docs/billing/listBillingPaymentMethods)
`GET /v1/orgs/{orgId}/billing/payment-methods`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Saved cards
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/billing/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Billing [#billing]
Prepaid balance, saved cards, deposits, and auto-recharge.
# Set the org's auto-recharge policy (/docs/billing/updateAutoRecharge)
`PUT /v1/orgs/{orgId}/billing/auto-recharge`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `enabled` boolean (required) — When on, the balance is topped up automatically below the threshold.
- `thresholdCents` integer (required) — Recharge fires when available balance drops below this.
- `amountCents` integer (required) — Amount charged per auto-recharge (within deposit bounds $10–$500).
## Responses
- `200` — Saved settings
- `400` —
- `401` —
- `403` —
- `503` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# GPU availability by model (/docs/catalog/getGpuAvailability)
`GET /v1/gpu-availability`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — Availability by gpu model
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# GPU + CPU pricing (/docs/catalog/getPricing)
`GET /v1/pricing`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — Pricing
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# GPU model catalog (/docs/catalog/listGpuModels)
`GET /v1/gpu-models`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — GPU models
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Locations / regions (/docs/catalog/listLocations)
`GET /v1/locations`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — Locations
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# VM template catalog (/docs/catalog/listVmTemplates)
`GET /v1/vm-templates`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — Templates
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/catalog/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Catalog [#catalog]
Public catalog: GPU models, availability, pricing, templates, and locations.
# Create a cluster (/docs/clusters/createCluster)
`POST /v1/orgs/{orgId}/clusters/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `resourceSize` string
- `tier` string
- `replicaCount` integer
- `gpuModelId` string
- `gpusPerReplica` integer
- `imageUrl` string
- `containerPort` integer
- `preferredRegions` array
- `allowFallback` boolean
- `envVars` object
- `registryCredentialId` string
- `healthChecks` array
- `healthCheckPath` string
- `healthCheckPort` integer
- `diskSizeGb` integer
- `containerCommand` array
- `privileged` boolean
- `kernelModules` array
- `templateId` string
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a cluster by id (/docs/clusters/getCluster)
`GET /v1/clusters/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — The cluster
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a cluster with its replicas and GPU model (/docs/clusters/getClusterDetail)
`GET /v1/clusters/{id}/detail`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — The cluster detail
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's clusters (cursor-paginated) (/docs/clusters/listOrgClusters)
`GET /v1/orgs/{orgId}/clusters`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `limit` (query) integer
- `cursor` (query) string
## Responses
- `200` — Page of clusters
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/clusters/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Clusters [#clusters]
Autoscaling inference clusters that serve a container image behind an endpoint.
# Restart a cluster (/docs/clusters/restartCluster)
`POST /v1/clusters/{id}/restart`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Scale a cluster's replica count (/docs/clusters/scaleCluster)
`POST /v1/clusters/{id}/scale`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Request body
`application/json`:
- `replicaCount` integer (required)
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Stop a cluster (/docs/clusters/stopCluster)
`POST /v1/clusters/{id}/stop`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Terminate a cluster (/docs/clusters/terminateCluster)
`POST /v1/clusters/{id}/terminate`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Add a member by email (owner/admin) (/docs/organizations/addOrgMember)
`POST /v1/orgs/{orgId}/members/add`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `email` string (required)
- `role` string (required)
## Responses
- `200` — Added
- `401` —
- `403` —
- `404` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Create an organization (caller becomes owner). Supabase-JWT only. (/docs/organizations/createOrg)
`POST /v1/orgs`
Authentication: Bearer API key (`vl_…`)
## Request body
`application/json`:
- `name` string (required)
- `slug` string
- `billingEmail` string
## Responses
- `200` — Created (caller is owner)
- `401` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get an organization (/docs/organizations/getOrg)
`GET /v1/orgs/{orgId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — The organization
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's members (/docs/organizations/listOrgMembers)
`GET /v1/orgs/{orgId}/members`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Members
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/organizations/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Organizations [#organizations]
Organizations and their members. Most resources are scoped to an org.
# Remove a member (owner/admin) (/docs/organizations/removeOrgMember)
`DELETE /v1/orgs/{orgId}/members/{userId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `userId` (path, required) string
## Responses
- `204` — Removed
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Update an organization (owner/admin) (/docs/organizations/updateOrg)
`PATCH /v1/orgs/{orgId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string
- `billingEmail` string
## Responses
- `200` — Updated
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Update a member's role (owner/admin) (/docs/organizations/updateOrgMemberRole)
`PATCH /v1/orgs/{orgId}/members/{userId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `userId` (path, required) string
## Request body
`application/json`:
- `role` string (required)
## Responses
- `200` — Updated
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Submit a provider application (/docs/provider/applyForProvider)
`POST /v1/orgs/{orgId}/provider/apply`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `note` string (required)
## Responses
- `204` — Submitted
- `400` —
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Generate a one-time provisioning token (owner/admin, provider only) (/docs/provider/generateProvisioningToken)
`POST /v1/orgs/{orgId}/provider/tokens`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string
- `expiresInHours` integer
- `nodeType` string
- `maxNodes` integer — Enrollment quota; 0/omitted = unlimited
- `nodePool` string — Pool enrolled nodes land in (default community)
## Responses
- `200` — Created
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Provider earnings from usage records (+ payout history) (/docs/provider/getProviderEarnings)
`GET /v1/orgs/{orgId}/provider/earnings`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Provider node + token stats (/docs/provider/getProviderStats)
`GET /v1/orgs/{orgId}/provider/stats`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Provider status (isProvider + application status) (/docs/provider/getProviderStatus)
`GET /v1/orgs/{orgId}/provider/status`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List the provider org's nodes (with GPUs + location) (/docs/provider/listProviderNodes)
`GET /v1/orgs/{orgId}/provider/nodes`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `includeRemoved` (query) boolean
- `status` (query) string — Filter to one status (online|offline|maintenance|draining|removed)
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List the org's provisioning tokens (provider only) (/docs/provider/listProvisioningTokens)
`GET /v1/orgs/{orgId}/provider/tokens`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/provider/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Provider [#provider]
For GPU providers: applications, nodes, provisioning tokens, and earnings.
# Revoke a provisioning token (owner/admin) (/docs/provider/revokeProvisioningToken)
`DELETE /v1/orgs/{orgId}/provider/tokens/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `204` — Revoked
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Set a node's operator lifecycle status (drain / maintenance / remove / online) (/docs/provider/updateProviderNode)
`PATCH /v1/orgs/{orgId}/provider/nodes/{nodeId}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `nodeId` (path, required) string
## Request body
`application/json`:
- `status` string (required) — Operator-set lifecycle state. draining/maintenance stop new placement (reconcile leaves them alone, capacity excluded); removed decommissions the node (Phase-1 reaper frees its workloads + GPU units); online clears an operator hold (the next reconcile pass re-derives true liveness from Nomad).
## Responses
- `200` — OK
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Create a registry credential (credentials stored encrypted) (/docs/registry-credentials/createOrgRegistryCredential)
`POST /v1/orgs/{orgId}/registry-credentials/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `server` string (required)
- `authType` string (required)
- `credentials` object (required)
## Responses
- `200` — Created
- `400` — Invalid request
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Delete a registry credential (/docs/registry-credentials/deleteOrgRegistryCredential)
`DELETE /v1/orgs/{orgId}/registry-credentials/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `204` — Deleted
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's registry credentials (no secrets) (/docs/registry-credentials/listOrgRegistryCredentials)
`GET /v1/orgs/{orgId}/registry-credentials`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Registry credentials
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/registry-credentials/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Registry Credentials [#registry-credentials]
Private container-registry credentials for pulling images.
# Create a runner pool (/docs/runners/createRunnerPool)
`POST /v1/orgs/{orgId}/runner-pools`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `installationId` integer (required)
- `githubOwner` string
- `githubOwnerType` string
- `poolRamGb` integer (required)
- `diskSizeGb` integer
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Delete (or drain) a runner pool (/docs/runners/deleteRunnerPool)
`DELETE /v1/orgs/{orgId}/runner-pools/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Runner pool job metrics (last 30 days) + cost comparison (/docs/runners/getRunnerJobMetrics)
`GET /v1/orgs/{orgId}/runner-pools/{id}/metrics`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a runner pool (with usage) (/docs/runners/getRunnerPool)
`GET /v1/orgs/{orgId}/runner-pools/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List runner jobs for a pool (/docs/runners/listRunnerJobs)
`GET /v1/orgs/{orgId}/runner-pools/{id}/jobs`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
- `limit` (query) integer
- `offset` (query) integer
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List runner pools (with active/queued usage) (/docs/runners/listRunnerPools)
`GET /v1/orgs/{orgId}/runner-pools`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/runners/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Runners [#runners]
Managed GitHub Actions runner pools.
# Resize a runner pool's RAM tier (/docs/runners/resizeRunnerPool)
`POST /v1/orgs/{orgId}/runner-pools/{id}/resize`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Request body
`application/json`:
- `poolRamGb` integer (required)
## Responses
- `200` — OK
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Create a snapshot of a VM (/docs/snapshots/createVmSnapshot)
`POST /v1/vms/{id}/snapshots`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Request body
`application/json`:
- `name` string (required)
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
- `404` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Soft-delete a snapshot (/docs/snapshots/deleteSnapshot)
`DELETE /v1/snapshots/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `204` — Deleted
- `401` —
- `403` —
- `404` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Fork a new VM from a snapshot (/docs/snapshots/forkVm)
`POST /v1/orgs/{orgId}/vms/fork`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `snapshotId` string (required)
- `name` string
- `gpuModelId` string
- `gpuCount` integer
- `resourceSize` string
- `diskSizeGb` integer
- `imageUrl` string
- `envVars` object
- `containerPort` integer
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's VM snapshots (optionally filtered by source VM) (/docs/snapshots/listOrgSnapshots)
`GET /v1/orgs/{orgId}/snapshots`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `vmId` (query) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/snapshots/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Snapshots [#snapshots]
Point-in-time VM snapshots and forking new VMs from them.
# Add an SSH public key (computes fingerprint) (/docs/ssh-keys/createOrgSshKey)
`POST /v1/orgs/{orgId}/ssh-keys/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `publicKey` string (required)
## Responses
- `200` — Added
- `400` — Invalid key
- `401` —
- `403` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Delete an SSH public key (/docs/ssh-keys/deleteOrgSshKey)
`DELETE /v1/orgs/{orgId}/ssh-keys/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `204` — Deleted
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's SSH public keys (/docs/ssh-keys/listOrgSshKeys)
`GET /v1/orgs/{orgId}/ssh-keys`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — SSH keys
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/ssh-keys/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## SSH Keys [#ssh-keys]
Org-level SSH public keys that can be attached to VMs.
# Accept a pending incoming transfer (target org owner/admin) (/docs/transfers/acceptTransfer)
`POST /v1/transfers/{id}/accept`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Request body
`application/json`:
- `sshKeyIds` array
## Responses
- `204` — Accepted
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Cancel a pending outgoing transfer (source org) (/docs/transfers/cancelTransfer)
`POST /v1/transfers/{id}/cancel`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `204` — Cancelled
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Count of pending incoming transfers (/docs/transfers/getTransferPendingCount)
`GET /v1/orgs/{orgId}/transfers/pending-count`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Initiate a resource transfer to another org (owner/admin) (/docs/transfers/initiateTransfer)
`POST /v1/orgs/{orgId}/transfers`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `resourceType` string (required)
- `resourceId` string (required)
- `targetOrgSlug` string (required)
- `note` string
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Pending incoming transfers (enriched) (/docs/transfers/listIncomingTransfers)
`GET /v1/orgs/{orgId}/transfers/incoming`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Outgoing transfers, all statuses (enriched) (/docs/transfers/listOutgoingTransfers)
`GET /v1/orgs/{orgId}/transfers/outgoing`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Orgs the caller belongs to (for the transfer target dropdown) (/docs/transfers/listTransferTargetOrgs)
`GET /v1/transfers/target-orgs`
Authentication: Bearer API key (`vl_…`)
## Responses
- `200` — OK
- `401` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/transfers/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Transfers [#transfers]
Move resources between organizations you belong to.
# Reject a pending incoming transfer (target org owner/admin) (/docs/transfers/rejectTransfer)
`POST /v1/transfers/{id}/reject`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `204` — Rejected
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Current-month usage (cents/hours + per gpuModel-tier breakdown) (/docs/usage/getCurrentUsage)
`GET /v1/orgs/{orgId}/usage/current`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/usage/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## Usage [#usage]
Current-period usage and cost breakdowns.
# Attach an org SSH key to a VM (/docs/vms/attachVmSshKey)
`POST /v1/vms/{id}/ssh-keys`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Request body
`application/json`:
- `sshKeyId` string (required)
## Responses
- `204` — Attached
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Create a VM (/docs/vms/createVm)
`POST /v1/orgs/{orgId}/vms/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `resourceSize` string
- `tier` string
- `gpuModelId` string
- `gpuCount` integer
- `imageUrl` string
- `containerPort` integer
- `preferredRegions` array
- `allowFallback` boolean
- `envVars` object
- `registryCredentialId` string
- `healthChecks` array
- `healthCheckPath` string
- `healthCheckPort` integer
- `diskSizeGb` integer
- `containerCommand` array
- `privileged` boolean
- `kernelModules` array
- `templateId` string
- `sshKeyIds` array
- `public` boolean — true = open endpoint (no data-plane auth); default false requires an org API key
## Responses
- `200` — Created
- `400` —
- `401` —
- `403` —
- `409` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a VM by id (/docs/vms/getVm)
`GET /v1/vms/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — The VM
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a VM with its GPU/node/template info and price (/docs/vms/getVmDetail)
`GET /v1/vms/{id}/detail`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — The VM detail
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# SSH keys attached to a VM + the org's keys (/docs/vms/getVmSshKeys)
`GET /v1/vms/{id}/ssh-keys`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Get a VM's embedded Telegram link (/docs/vms/getVmTelegramLink)
`GET /v1/vms/{id}/telegram-link`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's VMs (cursor-paginated) (/docs/vms/listOrgVms)
`GET /v1/orgs/{orgId}/vms`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `limit` (query) integer
- `cursor` (query) string
## Responses
- `200` — Page of VMs
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/vms/overview)
{/* AUTO-GENERATED by scripts/build-openapi.mjs — edit the tag list there, not here. */}
## VMs [#vms]
GPU virtual machines: lifecycle, disks, SSH access, and console links.
# Reboot a VM (/docs/vms/rebootVm)
`POST /v1/vms/{id}/reboot`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `204` — Rebooted
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Resize a stopped VM's disk (/docs/vms/resizeVmDisk)
`POST /v1/vms/{id}/resize-disk`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Request body
`application/json`:
- `newSizeGb` integer (required)
## Responses
- `204` — Resized
- `400` —
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Restart a VM (/docs/vms/restartVm)
`POST /v1/vms/{id}/restart`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Stop a VM (/docs/vms/stopVm)
`POST /v1/vms/{id}/stop`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Terminate a VM (/docs/vms/terminateVm)
`POST /v1/vms/{id}/terminate`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `id` (path, required) string
## Responses
- `200` — OK
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Create a webhook (returns the signing secret once) (/docs/webhooks/createOrgWebhook)
`POST /v1/orgs/{orgId}/webhooks/create`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Request body
`application/json`:
- `name` string (required)
- `url` string (required)
- `events` array
## Responses
- `200` — Created
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Delete a webhook (/docs/webhooks/deleteOrgWebhook)
`DELETE /v1/orgs/{orgId}/webhooks/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `204` — Deleted
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Recent delivery attempts for a webhook (/docs/webhooks/listOrgWebhookDeliveries)
`GET /v1/orgs/{orgId}/webhooks/{id}/deliveries`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
- `limit` (query) integer
## Responses
- `200` — Deliveries
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# List an org's webhooks (no secrets) (/docs/webhooks/listOrgWebhooks)
`GET /v1/orgs/{orgId}/webhooks`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
## Responses
- `200` — Webhooks
- `401` —
- `403` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Overview (/docs/webhooks/overview)
## Webhooks [#webhooks]
Webhooks let you react to events — a cluster coming online, a VM stopping —
without polling. OpenRelay `POST`s a JSON payload to a URL you control, signs
each delivery, and retries on failure.
## Create a webhook [#create-a-webhook]
Register an endpoint and the events you care about. The response includes the
**signing secret** (`whsec_…`), shown **once**:
```bash
curl -X POST https://api.openrelay.inc/v1/orgs/$ORG_ID/webhooks/create \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "deploys",
"url": "https://example.com/hooks/openrelay",
"events": ["cluster.running", "cluster.failed"]
}'
```
```json
{
"id": "wh_…",
"url": "https://example.com/hooks/openrelay",
"events": ["cluster.running", "cluster.failed"],
"enabled": true,
"secret": "whsec_…", // store this now — shown once
"secretPrefix": "whsec_abcd"
}
```
You can't read a secret again, but you can roll it with
[`/webhooks/{id}/regenerate-secret`](/docs/webhooks/regenerateOrgWebhookSecret).
An empty `events` array subscribes to **all** events.
## Event types [#event-types]
| Event | Fires when |
| -------------------- | ------------------------------------------- |
| `cluster.running` | A cluster reaches a healthy, serving state. |
| `cluster.degraded` | Some replicas are unhealthy. |
| `cluster.failed` | A cluster fails. |
| `replica.healthy` | A replica becomes healthy. |
| `replica.unhealthy` | A replica becomes unhealthy. |
| `replica.terminated` | A replica is terminated. |
| `vm.running` | A VM reaches the running state. |
| `vm.stopped` | A VM is stopped. |
| `vm.terminated` | A VM is terminated. |
## Payload [#payload]
Every delivery is a JSON body of this shape:
```json
{
"id": "evt_…",
"type": "cluster.running",
"created_at": "2026-06-10T12:00:00Z",
"data": { "clusterId": "cl_…" }
}
```
Each request also carries these headers:
| Header | Description |
| ----------------------- | ------------------------------------------------ |
| `X-VectorLay-Signature` | `sha256=` HMAC of the raw body (see below). |
| `X-VectorLay-Event` | The event `type`, e.g. `cluster.running`. |
| `X-VectorLay-Delivery` | Unique id for this delivery attempt. |
## Verify the signature [#verify-the-signature]
Treat unsigned or badly-signed requests as forgeries. Verify the signature on
the **raw request body** before parsing it.
The signature is computed as:
```
X-VectorLay-Signature = "sha256=" + hex( HMAC_SHA256(key, rawBody) )
```
where the HMAC **key** is the lowercase hex SHA-256 of your `whsec_…` secret —
i.e. `hex(sha256(secret))`. Compute the key once and reuse it.
```ts title="Node.js / TypeScript"
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
// Derive the signing key from your stored secret (do this once).
function signingKey(secret: string) {
return createHash('sha256').update(secret).digest('hex');
}
export function verify(rawBody: string, header: string, secret: string) {
const expected =
'sha256=' +
createHmac('sha256', signingKey(secret)).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && timingSafeEqual(a, b);
}
```
```python title="Python"
import hashlib, hmac
def signing_key(secret: str) -> bytes:
return hashlib.sha256(secret.encode()).hexdigest().encode()
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(signing_key(secret), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
```
Compute the HMAC over the exact bytes you received, **before** any JSON
parsing or re-serialization — reformatting the body changes the signature.
## Responding, retries & delivery [#responding-retries--delivery]
* **Respond `2xx` quickly.** Acknowledge fast and do slow work asynchronously;
receivers that take too long are treated as failures.
* **Retries.** Failed deliveries are retried up to **3 times** with backoff
(\~10s, 60s, 300s). Use `X-VectorLay-Delivery` to **deduplicate** — the same
event may arrive more than once.
* **Inspect deliveries** with
[`/webhooks/{id}/deliveries`](/docs/webhooks/listOrgWebhookDeliveries) to see
recent attempts and response codes.
* **Pause** a webhook with
[`/webhooks/{id}/toggle`](/docs/webhooks/toggleOrgWebhook) instead of deleting it.
# Regenerate a webhook's signing secret (returned once) (/docs/webhooks/regenerateOrgWebhookSecret)
`POST /v1/orgs/{orgId}/webhooks/{id}/regenerate-secret`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Responses
- `200` — Regenerated
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Enable/disable a webhook (/docs/webhooks/toggleOrgWebhook)
`POST /v1/orgs/{orgId}/webhooks/{id}/toggle`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Request body
`application/json`:
- `enabled` boolean (required)
## Responses
- `200` — Toggled
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json
# Update a webhook (name/url/events) (/docs/webhooks/updateOrgWebhook)
`PATCH /v1/orgs/{orgId}/webhooks/{id}`
Authentication: Bearer API key (`vl_…`)
## Parameters
- `orgId` (path, required) string
- `id` (path, required) string
## Request body
`application/json`:
- `name` string
- `url` string
- `events` array
## Responses
- `200` — Updated
- `401` —
- `403` —
- `404` —
Full machine-readable specification: https://docs.openrelay.inc/openapi.json