# 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