---
name: Partner API
audience: public
---

# Partner API

Public guide for hosted agents (Muse first). This file is the source for [/docs/partners](/docs/partners) and the raw markdown at [/docs/partners.md](/docs/partners.md) — read this URL; you do not need to call the API first.

The Partner API lets a hosted agent that cannot run MCP create and iterate Fifth Door games from a natural-language prompt. It is REST over HTTPS, generated from Fifth Door's oRPC contract. Builds run asynchronously on Fifth Door's existing Studio turn pipeline. You get a play URL when the game is ready, and images when the builder asks about a look before the first publish.

If you are a person building in your own editor, use the [MCP server](/docs/mcp) instead.

## Base URL

```
https://api.beta.fifthdoor.com/api/partners/v1
```

Machine-readable spec (no auth): [`/openapi.json`](https://api.beta.fifthdoor.com/api/partners/v1/openapi.json). This guide is also raw markdown at [`/docs/partners.md`](/docs/partners.md).

## Authenticate

Hosted agents (Muse) should use OAuth 2.1 authorization code + PKCE so the player can sign in or create a Fifth Door account in a browser window. Personal API keys (`fd_`) still work for scripts.

### OAuth (Muse)

1. Discover the authorization server at [`/.well-known/oauth-authorization-server`](https://api.beta.fifthdoor.com/.well-known/oauth-authorization-server) (no auth). Protected-resource metadata is at [`/.well-known/oauth-protected-resource`](https://api.beta.fifthdoor.com/.well-known/oauth-protected-resource).
2. Register a public client (`token_endpoint_auth_method: "none"`) against [`POST /api/oauth/register`](https://api.beta.fifthdoor.com/api/oauth/register) with your `redirect_uris`. HTTPS is required except `http://localhost` / `http://127.0.0.1`.
3. Open the `authorization_endpoint` (`/oauth/authorize` on the Fifth Door web origin) with `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, and `code_challenge_method=S256`. The player signs in or signs up, then Allow.
4. Exchange the code at `POST /api/oauth/token` (`application/x-www-form-urlencoded` or JSON) with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, and `code_verifier`.
5. Send the returned `access_token` as `Authorization: Bearer` on Partner API requests.

The access token carries the `partner` scope and is valid for up to one hour (`expires_in` reports the remaining seconds). It reaches this API only: it cannot publish bundles directly or change artifact visibility under `/api/platform`. OAuth apps can list and change only games created by their own `client_id`; personal API keys retain account-wide access to partner-created games. Sessions without an app association remain accessible in Studio or with a personal API key.

Token responses include a `refresh_token`. Exchange it at `POST /api/oauth/token` with `grant_type=refresh_token`, `refresh_token`, and `client_id`. Save both replacement tokens atomically before another request: refresh tokens are single-use, rotation invalidates the previous access token, and replay revokes the connection. Serialize refresh requests. The connection lasts at most 30 days from authorization, after which the player must authorize again. A lost refresh response also requires reauthorization.

Re-authorizing the same `client_id` supersedes its previous grant. Disconnect with `POST /api/oauth/revoke`, passing `token` (access or refresh token) and `client_id`; it returns 200 for unknown or already revoked tokens too. The player can also disconnect under **Connected apps** on their [account page](/account). Disconnect blocks future API access; it does not delete games or cancel builds already accepted.

Authorization and token requests may include `resource=https://api.beta.fifthdoor.com/api/partners/v1`. Other resources and scopes are rejected. Omitting `resource` selects this API. Self-registered clients are visibly marked **Unverified app** until Fifth Door verifies their identity.

A 401 from the Partner API includes `WWW-Authenticate` pointing at the protected-resource metadata.

### API keys

Create a personal API key on the Fifth Door [account page](/account). Keys start with `fd_`. Send it as a Bearer token on every request except `/` and `/openapi.json`:

```
Authorization: Bearer fd_your_key
```

`X-API-Key: fd_your_key` is also accepted.

The token belongs to a full Fifth Door account. Guest or anonymous users get `403`. If the account has not accepted the current terms, the API returns `403` with instructions to sign in on the web and accept, then retry.

`GET /me` confirms the token:

```bash
curl -sS \
  -H "Authorization: Bearer fd_your_key" \
  https://api.beta.fifthdoor.com/api/partners/v1/me
```

```json
{
  "userId": "…",
  "username": "creator",
  "email": "creator@example.com",
  "name": "Creator"
}
```

## Create a game

`POST /games` with a prompt and an `Idempotency-Key` header. The prompt is 1–8000 characters after trim.

```bash
curl -sS -X POST \
  -H "Authorization: Bearer fd_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-memory-match-1" \
  -d '{"prompt":"Make a memory-match game with animal cards"}' \
  https://api.beta.fifthdoor.com/api/partners/v1/games
```

`201` and a game object. Status is `building`. `playUrl` is `null` until the first successful publish.

```json
{
  "id": "museGame01",
  "status": "building",
  "title": null,
  "playUrl": null,
  "message": null,
  "messageId": null,
  "images": [],
  "studioPath": "/sessions/museGame01/chat",
  "source": "muse"
}
```

## Poll until the turn finishes

Poll `GET /games/{id}` while `status` is `building`. Turns can take several minutes. Do not send another prompt while a turn is building.

Once the turn finishes, deliver any new `message` to the player, remembering `messageId` to avoid repeating it on subsequent polls or reconnects. Show `images` when present — a look-approval turn may have no `playUrl` yet, and the picture is the thing to react to. Offer `playUrl` whenever available: a `ready` game can still ask a question about an edit. A look question with neither `images` nor `playUrl` is incomplete. Send the player's answer to `POST /games/{id}/messages` with a fresh idempotency key, then resume polling. Handle `interrupted` explicitly; the player can retry with another prompt or inspect Studio.

```bash
curl -sS \
  -H "Authorization: Bearer fd_your_key" \
  https://api.beta.fifthdoor.com/api/partners/v1/games/museGame01
```

```json
{
  "id": "museGame01",
  "status": "ready",
  "title": "Animal Match",
  "playUrl": "https://play.fifthdoor.com/play/animal-match",
  "message": "Animal Match is ready to play!",
  "messageId": "assistant-1:respond:reply-1",
  "images": [],
  "studioPath": "/sessions/museGame01/chat",
  "source": "muse"
}
```

A look question before publishing can return:

```json
{
  "id": "museGame01",
  "status": "idle",
  "title": "A Jar of Dusk",
  "playUrl": null,
  "message": "Here's the soft, painted look. Does this feel right? [options: A) Looks good, build it (recommended) B) Make dusk darker]",
  "messageId": "assistant-2:respond:reply-1",
  "images": [
    { "url": "https://api.beta.fifthdoor.com/api/platform/style-refs/museGame01/style-reference.png" }
  ],
  "studioPath": "/sessions/museGame01/chat",
  "source": "muse"
}
```

`message` is the latest player-facing reply from the most recent player turn, including labeled question options and suggested next steps. It excludes internal tool activity. `messageId` is an opaque, stable reply identifier. Both fields are `null` while building or when that turn has no visible reply; an older answered question is never substituted. An `idle` response with no message has no reply to deliver; inspect Studio or send another prompt rather than polling indefinitely. A message can also be a completion reply, so its presence alone does not mean the builder is waiting for an answer.

`images` are absolute URLs for that same reply: the style reference on a look question, or playtest frames from that turn when the reply did not attach its own. Empty while building, on list/create/iterate, and when the turn has no durable picture. Fetch them as images; they do not require a Partner token. Do not approve a look from text alone.

`studioPath` is a path on the Fifth Door web origin. The account that owns the API key can open it in Studio.

## Status

| Status | Meaning |
| --- | --- |
| `building` | A turn is in progress. Keep polling. |
| `ready` | The game is published. `playUrl` is an absolute play URL. |
| `idle` | The last turn finished without a published play URL. Send another prompt or inspect the session in Studio. |
| `interrupted` | The last turn stopped. You can send another prompt. |

## Iterate

`POST /games/{id}/messages` sends another builder turn (answer a question, change difficulty, add a mechanic, fix a bug). Same prompt limits as create.

```bash
curl -sS -X POST \
  -H "Authorization: Bearer fd_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: iterate-memory-match-1" \
  -d '{"prompt":"Make it harder and add a timer"}' \
  https://api.beta.fifthdoor.com/api/partners/v1/games/museGame01/messages
```

`200` and the game with `status: "building"`. If a turn is already running, the API returns `409`:

```json
{
  "defined": true,
  "code": "CONFLICT",
  "status": 409,
  "message": "Wait until the current build finishes, then send your change"
}
```

Poll `GET /games/{id}` until status is not `building`, then retry.

## List games

`GET /games` lists games this OAuth app created for the account. Personal API keys list all games this account created through the Partner API. Studio-created sessions are not included. List and create/iterate responses keep `message`, `messageId`, and `images` empty; fetch a game by ID to read its latest reply.

```json
{ "games": [{ "id": "museGame01", "status": "ready", "title": "Animal Match", "playUrl": "https://play.fifthdoor.com/play/animal-match", "message": null, "messageId": null, "images": [], "studioPath": "/sessions/museGame01/chat", "source": "muse" }] }
```

## Safe retries

Every create or iterate request requires `Idempotency-Key` (1–128 letters, numbers, dots, underscores, colons or hyphens). Use a fresh key for each intentional action and the same key and body for retries. Reusing a key for a different game or prompt returns 409. Completed requests retain their original response for 30 days; poll the game for its current status. Retrying does not consume another build allowance.

The turn and retry record are saved before dispatch. If dispatch fails, retry with the same key; the background worker also retries pending dispatches. Concurrent changes to one game return 409 rather than superseding an active build.

## Visibility and data

Creating and iterating are write actions that run an AI builder and may publish a playable bundle. Share links may be accessible to anyone who has the link; unlisted does not mean private. Studio edits after a public release may be staged for review rather than immediately changing the released game. The API cannot change visibility or promote a draft; use Studio for those actions.

`GET /me` shares the account's name, email, username and ID with the connected app. Prompts become part of the stored Studio conversation and are processed by the configured AI providers and observability systems. Do not include secrets or unrelated personal data in prompts. See the [privacy policy](/privacy) and [terms](/tos) for governing policies; account deletion is available on the web.

## Rate limits and errors

Create and iterate each count as one turn. The limit is 10 turns per hour per account. Over the limit: `429` with `code: "TOO_MANY_REQUESTS"`.

OAuth endpoints accept bodies up to 16 KiB, with 10 registrations or 120 other requests per minute per IP and an additional global registration cap. OAuth 429 responses include `Retry-After: 60`. Partner request bodies are limited to 40 KiB.

Partner procedure errors use the oRPC body: `{ defined, code, status, message }`.

| Status | When |
| --- | --- |
| `400` | Invalid JSON, empty / overlong prompt, or missing / invalid idempotency key. |
| `401` | Missing or invalid token. `WWW-Authenticate` names the OAuth authorization server. |
| `403` | Guest account, or terms not accepted on the web. |
| `404` | Unknown `id`, or the game is not owned by this account, or it was not created through this API. |
| `409` | Iterate while `status` is `building`, or reuse an idempotency key with different input. |
| `413` | Request body too large. |
| `429` | Turn rate limit. |
| `500` | Dispatch pending. Retry with the same idempotency key. |

## What you should not do

- Do not call Studio oRPC or the MCP server from a hosted connector. This REST surface is the one for that.
- Do not create a second builder. A create or iterate is one `enqueueTurn` on the existing pipeline.
- Do not assume a game is playable because `POST` returned `201`. Wait for `ready` and a non-null `playUrl`.
- Do not answer a look question from text alone. Show `images` when present.
