---
id: platform/api
title: REST & GraphQL API
description: Authenticate to the Bex API, make REST and GraphQL requests, and handle workspace access, asynchronous operations, and errors.
keywords: [bex, rest api, graphql, api reference, bearer token, api keys]
last_updated: 2026-09-23
---

Use the Bex API to automate service deployments, configuration, and operations.
REST, GraphQL, and MCP share core operations, but their request shapes and
available actions differ. Consult the guide for the operation you need; an
endpoint accepting a request does not prove the resource is ready.

| Interface | Hosted endpoint | Use |
| --- | --- | --- |
| REST | `https://api.bex.co/v1/` | Scripts and integrations |
| GraphQL | `https://api.bex.co/graphql` | Structured queries and dashboard operations |
| MCP | `https://api.bex.co/mcp` | [Agent tools](./connect-an-agent.md) |

For a self-hosted installation, use its API and OAuth origins. The operator-only
[quickstart](./quickstart.md) does not set up all these services.

The [Bex Website Content API](https://bex.co/content/openapi.json) describes
public documentation and blog reads on `bex.co`. It is separate from these
authenticated platform interfaces and does not describe service or database
management. For the graded REST operation catalogue (method, path, summary,
implementation grade) and interactive explorer, see the
[API reference](./api-reference.md). Platform contracts are maintained in the
[Bex API source](https://github.com/bex-co/bex/tree/main/lego/backend/internal).

## Get a bearer token

Create an API key in the dashboard's API Keys settings while signed in as an
eligible developer or admin. Save its `id` and `secret` when it is created;
the secret is returned only once. A Bex API key is an OAuth client credential
pair: **the secret itself is not a bearer token**.

Set `BEX_KEY_ID` and `BEX_KEY_SECRET` from your secret store, then exchange them:

```bash
export BEX_API_URL='https://api.bex.co'
export BEX_OAUTH_TOKEN_URL='https://oauth.bex.co/oauth2/token'

BEX_TOKEN=$(curl --fail-with-body --silent --show-error \
  "$BEX_OAUTH_TOKEN_URL" \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=$BEX_KEY_ID" \
  --data-urlencode "client_secret=$BEX_KEY_SECRET" \
  | jq -er '.access_token')
export BEX_TOKEN

curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $BEX_TOKEN" \
  "$BEX_API_URL/v1/services"
```

This example requires `curl` and `jq`. Keep shell tracing off around credentials.
Use the token response's `expires_in` to schedule another exchange; do not assume
an access token is permanent. Client-credentials tokens are renewed by exchanging
the key again, rather than by a browser login.

The API also accepts a valid dashboard session when no bearer credential is
supplied. A supplied bearer credential takes precedence, including when it is
invalid. Browser sessions and user-consented OAuth clients have different
credential-issuance permissions from machine callers.

API-key management uses `POST /v1/api-keys` with `name` and optional `ownerId`,
`GET /v1/api-keys?ownerId=…`, and `DELETE /v1/api-keys/{id}?ownerId=…`.
Creating a durable key requires an authorized direct human caller; an existing
machine key or third-party agent cannot mint another key for itself. Rotate by
creating a replacement, updating consumers, and revoking the old key.

## Workspaces and permissions

Machine API keys are bound to their workspace with the **developer** role.
Creating one as an admin does not give the key admin permissions. Workspace
administration, member invitations, GitHub connections, webhooks, and audit-log
access require a suitable user credential instead. Use the dashboard's admin
session or a user-authorized OAuth token with the required role and scopes for
those operations; the client-credentials recipe above does not provide that role.
See [User-authorized OAuth integrations](./oauth-integrations.md) for a
downloadable PKCE sample.

Use IDs returned by the API. Where an endpoint supports `ownerId`, supply the
intended workspace explicitly, especially if your account belongs to several.
By-ID requests must still pass authorization for the resource's workspace.

[Workspace roles](./members.md) control resource access. Consented OAuth clients
also need the relevant scopes: `bex.read`, `bex.write`, and/or `bex.sensitive`.
A token scope does not grant a role the account lacks. Reading credentials,
environment variables, or other sensitive values requires separate access from
ordinary resource reads.

## REST requests

Service listings use entries containing a `service` object and `cursor`.
Follow the endpoint's pagination fields instead of treating one page as the
whole workspace. Default page size is 20 and the maximum is 100. Pass the last
entry's `cursor` as the opaque `cursor` query parameter; an unknown cursor
returns an empty tail. Concurrent creates, deletes, or renames can change what
later pages contain — complete traversal of returned pages is not a
transactional inventory.

A runnable walker lives in
[`/examples/service-inventory/`](/examples/service-inventory/README.md) and is
shared with the [OAuth user client](/examples/oauth-user-client/README.md)
sample. The `suspended` field is a string enum: `suspended` or
`not_suspended`.

Use the returned service `id` for subsequent operations:

```bash
export BEX_SERVICE_ID='replace-with-service-id'

curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $BEX_TOKEN" \
  "$BEX_API_URL/v1/services/$BEX_SERVICE_ID"
```

`POST /v1/services` creates a service; a name already in use is a conflict,
not a redeploy. Use [deploy operations](./how-deploys-work.md) for an existing
service or the [Blueprint workflow](./app-resource.md) to converge a manifest.
After an accepted mutation, inspect the deployment or resource until it reaches
the expected state. Avoid blindly retrying creation after a network timeout:
check whether the first request created the resource. For CI that deploys a
tested container digest to an image-backed service, see
[CI deploys a tested image digest](./ci-image-deploy.md).

## GraphQL requests

GraphQL has its own field names and input types. For example:

```bash
curl --fail-with-body --silent --show-error \
  "$BEX_API_URL/graphql" \
  -H "Authorization: Bearer $BEX_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"query":"query { services { id type suspended url phase } }"}'
```

Inspect both `data` and `errors`: HTTP success alone does not establish that a
GraphQL operation succeeded. Use the schema supported by your installed Bex
version; Render's dashboard schema is not a compatibility guarantee for Bex.

## Bex-only endpoints

Endpoints Bex adds beyond Render's public REST API are documented in their
guides, not in the Render-parity [API reference](./api-reference.md):

- `GET /v1/usage` returns the calling workspace's month-to-date usage:
  per-resource meter totals, an advisory cost estimate, the billing projection
  for callers that may see it, and a coverage record (`complete` / `partial` /
  `unknown`) describing how complete the totals are. See
  [Month-to-date usage](./usage.md). Implemented in
  `lego/backend/internal/usage/rest.go` (`RegisterREST`); the same verb also
  answers GraphQL `Query.usage` and the MCP `get_usage` tool.

## Errors and compatibility {#errors-and-compatibility}

| Result | Next step |
| --- | --- |
| `400` | Inspect the error and check field names, types, and supported settings. |
| `401` | Renew credentials; verify API origin and OAuth configuration. |
| `402` | Payment method required for this create. A billing manager completes [Billing setup](./billing-setup.md); then retry. |
| `403` | Check workspace, role, OAuth scopes, and any protected-environment requirement. |
| `404` | Verify the endpoint and resource ID in the intended workspace. |
| `409` | Resolve a name or state conflict before retrying. |
| `429` | Honor `Retry-After` and back off; caller and workspace budgets can both apply. |
| `503` | Check whether a required platform backend is unavailable or unconfigured. |

Rate limits depend on installation configuration and the operation. They are
not a universal promise of 500 requests per minute for each key.

Bex implements many Render-shaped endpoints plus Bex extensions. Authentication,
unsupported fields, and operational behavior can differ; changing a Render
client's base URL is not sufficient migration validation. Start with
[compatibility](./compatibility.md) and [migration from Render](./migrate-from-render.md),
then verify each operation your integration uses.
