# The API

> One GraphQL endpoint for every Playcode operation - how to call it, how operations are named and found, errors, consent and idempotency.
> Source: https://playcode.io/docs/developers/api - last reviewed 2026-10-10.

Everything Playcode can do is an operation in one GraphQL API: the app, Playcode's agent, the CLI, MCP and the SDK all call it. Send a query to `https://playcode.io/api/graphql` with an access token, and the answer can do only what the token allows.

## Call it

```bash
curl https://playcode.io/api/graphql \
  -H 'Authorization: Bearer pc_live_...' \
  -H 'Content-Type: application/json' \
  -H 'x-playcode-via: api' \
  -d '{"query":"query { projectSearch(first: 5) { edges { node { uid name title projectType } } } }"}'
```

| Part | Value |
|---|---|
| Endpoint | `POST https://playcode.io/api/graphql` |
| Credential | `Authorization: Bearer` with an access token (`pc_live_...`), or the token of a tool signed in through Sign in with Playcode. See [Access tokens and permissions](/docs/developers/tokens-and-permissions) |
| `x-playcode-via` | Optional: `api` names your door in the workspace's [activity](/docs/developers/activity) |
| `Idempotency-Key` | Optional, on a write: see [Idempotency](#idempotency) |

## How operations are named

An operation is a resource and a verb: `projectGet`, `domainUpdate`, `projectPublishCreate`.

| Shape | Rule | Examples |
|---|---|---|
| The five verbs | `Create`, `Get`, `Search` (a list), `Update`, `Delete` | `projectGet`, `dnsZoneUpdate` |
| Actions | One consequential thing to one resource | `domainConnect`, `domainMove` |
| Jobs | Anything that can take longer: `Create` starts it, `Get` reads its status, `Cancel` stops it | `projectPublishCreate`, `commandRunCreate` |
| Settings | Fields of `Update`, never a verb of their own | `domainUpdate` with `autoRenew` |

Every operation has an effect: `READ` changes nothing, `WRITE` changes something, `MONEY` charges the workspace.

## Find an operation

The registry is in the API itself, free for every token:

| Operation | What it answers |
|---|---|
| `apiOperationSearch(query, family)` | The best matches for words like "buy a domain": each with its effect, whether it needs consent, the permission it needs and its arguments |
| `apiOperationGet(name)` | One operation: its arguments as a JSON schema, an example, what it returns, and a ready query document |

The CLI shows the same: `playcode <family>` lists a family, and `playcode <family> <operation> --help` prints the arguments. Over MCP, the tools are `search_operations` and `describe_operation`.

| Family | What it holds | Permission a token needs |
|---|---|---|
| `projects` | Projects, files, commands, publishes | `projects:read` to read; `projects:vm` to change |
| `domains` | Names to buy, domains, DNS zones, orders | `domains:read` to read; `domains:write` to change |
| `chats` | Chats with Playcode's agent in a project | `chats:read` to read; `chats:write` to write |
| `events` | The workspace's activity | None |
| `consent` | Proposals | None |
| `api` | The registry | None |
| `tokens`, `webhooks` | Access tokens, webhook endpoints | A signed-in person only, never a token |
| `apps` | An app's email, secrets and services | Not reachable with a token today |

## Errors

A refusal comes back in `errors`, each with `extensions.code` from a fixed list, a `reason`, and `metadata`:

```json
{ "errors": [ { "message": "...", "extensions": { "code": "missing_scope", "reason": "domainUpdate needs the domains:write permission", "metadata": { "needs": "domains:write" } } } ] }
```

| Code | Means |
|---|---|
| `invalid_key` | The token is unknown, revoked or expired |
| `missing_scope` | The token lacks a permission or does not reach the target. `metadata.needs` names the permission |
| `access_denied` | You have no access to this resource |
| `not_found` | No such resource |
| `validation` | An argument is wrong; `field` names it when it can |
| `consent_required` | The operation needs a proposal: see [Consent](#consent) |
| `proposal_drifted` | The proposal was made for other arguments |
| `proposal_expired` | The proposal is too old |
| `proposal_declined` | The proposal was declined |
| `proposal_consumed` | The proposal was already used |
| `idempotency_conflict` | The `Idempotency-Key` was used with other arguments |
| `idempotency_in_flight` | The call with this key is still running |
| `rate_limited` | Too many requests |
| `insufficient_balance` | The workspace is out of credits |
| `confirmation_required` | The person must confirm it's them again in the app |
| `killed`, `disabled` | Playcode stopped this traffic on purpose |
| `internal` | Something failed on Playcode's side |
| `refused` | The service refused; `reason` says why |

## Consent

An operation marked with consent, like buying a domain (`domainCreate`) or editing DNS records (`dnsZoneUpdate`), never applies on a bare call. It takes two:

1. `proposalCreate(operation, args)` with the operation's exact arguments. It answers the proposal's id and its `terms`, written by the service that will charge or change.
2. The operation itself, with the same arguments and `proposalId` set to that id.

A money proposal lives 15 minutes, any other 1 hour. Changed arguments need a new proposal, and a proposal works once. `proposalDecline` ends one.

The API does not ask anyone in between. A tool that acts for a person must show them the terms and wait for their yes before the second call: see [Docs for agents](/docs/developers/agents).

## Idempotency

Send `Idempotency-Key` with a write, and the API stores its answer for 24 hours: the same key with the same call answers the stored result instead of running again. The key is 1 to 255 characters of letters, digits, underscore, dot, colon or dash. For an operation that needs consent, the proposal plays this part.

## Limits

- A token works in one workspace. A call that names another workspace, or a project outside the token's reach, is refused.
- Reads do not land in the workspace's activity; every successful write does.
- Lists are bounded; each list operation's description states its bound.
- The generated reference of every operation is not published as a docs page yet: use `apiOperationSearch` and `apiOperationGet`.
