polypoint-theatre-api-contract.md
Polypoint Theatre API Contract
Version: 1.0 — 22 October 2026
This contract defines the server API for a plugin exposing exactly three tools: store_theatre, read_theatre, and remove_theatre. All actions use one POST endpoint. MUST indicates required behaviour. The domain and final endpoint path remain deployment configuration.
Endpoint and authentication
POST https://[domain]/llm_api/theatres/
Authorization: Bearer <api_key>
Content-Type: application/x-www-form-urlencoded
Accept: application/json
The server MUST accept UTF-8 URL-encoded form fields. Clients MUST use a form encoder so JavaScript containing +, &, =, newlines, or Unicode survives unchanged. The source is a text field, not a multipart file upload. HTTPS is required.
The API key MUST be sent only in the Authorization header, never a URL or form field. It identifies the caller and their personal workspace. All actions, including public reads, require authentication. Missing, invalid, expired, or revoked keys return 401. Server logs MUST redact credentials. Workspace ownership MUST be derived from authentication, never a caller-provided owner or path.
/llm_api/theatres/ is the proposed fixed path. If deployment instead uses /llm_api/[name]/, that path segment is configured once and MUST NOT act as the example selector.
Fields
| Field | Type | Meaning |
|---|---|---|
action |
string enum | store, read, or remove |
name |
string | Workspace-local example name; mutually exclusive with id |
id |
string | Permanent, globally unique, opaque example identifier |
theatre_file_content |
string | Complete JavaScript source for store |
scope |
string enum | Read-by-name namespace: personal (default) or public |
revision |
positive decimal integer | Expected current revision for update or removal |
Exactly one nonempty selector (name or id) is required. Duplicate fields, unknown fields, unsupported actions, and fields not applicable to an action return 400. IDs MUST be treated as opaque values, not filesystem paths.
Names are case-sensitive, 1–128 ASCII characters, matching [A-Za-z0-9][A-Za-z0-9_-]{0,127}. They are unique within the owner's workspace. No implicit trimming or case conversion occurs. A human phrase such as “my polypoint example” becomes my-polypoint-example at the client. Names cannot be changed through this API.
Maximum decoded UTF-8 source size: 1 MiB (1,048,576 bytes). Empty source is allowed. Oversized source returns 413. Infrastructure limits MUST accommodate form-encoding overhead for an allowed source.
Ownership and visibility
- A caller may read any public example and any example they own.
- A caller may store, update, or remove only examples they own.
- Personal means owned by the caller; it does not imply private visibility.
- New examples are private by default. Existing visibility is preserved by updates. This API has no publish or visibility action.
- Store and remove always resolve names in the caller's workspace and reject
scope. - Read by ID resolves that ID globally, then checks visibility or ownership. It rejects
scope. - Read by name defaults to the caller's workspace.
scope=publicsearches public examples across owners. One match succeeds; no matches return 404; multiple matches return 409ambiguous_name. The caller must then supply an ID obtained from a known example URL or another source; this API has no list/search action. - Missing examples and inaccessible examples both return 404, without disclosing private metadata.
- Public examples owned by someone else can be copied by reading their source and storing it under a name in the caller's workspace. Their IDs cannot be used as store targets.
Store
Tool: store_theatre(name | id, theatre_file_content, revision?)
Required fields: action=store, exactly one selector, and theatre_file_content. The content field MUST be present even when empty.
| Target | Revision | Result |
|---|---|---|
| New name | omitted | Create; HTTP 201; revision 1 |
| New name | supplied | 409 revision_conflict; no creation |
| Existing owned name or ID | matches current | Replace complete source; HTTP 200; increment revision |
| Existing owned name or ID | omitted | 409 revision_required; no change |
| Existing owned name or ID | stale | 409 revision_conflict; no change |
| Missing ID or another owner's ID | any | 404 not_found; no creation |
Every successful store of an existing example increments its revision, including when source bytes are unchanged. Updates preserve ID, name, owner, and visibility. Store MUST NOT execute the supplied source. Source is preserved exactly after UTF-8 form decoding, including whitespace and line endings.
Creation uniqueness checks and revision-checked writes MUST be atomic. Concurrent create requests for the same name cannot overwrite each other: the first creates, and a later request without a revision receives revision_required. Source replacement must be atomic; readers see either the previous complete source or the new complete source.
Example create (curl performs form encoding):
curl --request POST 'https://[domain]/llm_api/theatres/' \
--header "Authorization: Bearer ${POLYPOINT_API_KEY}" \
--data-urlencode 'action=store' \
--data-urlencode 'name=my-polypoint-example' \
--data-urlencode 'theatre_file_content=console.log("Hello Polypoint");'
HTTP 201:
{
"ok": true,
"id": "th_abc123",
"name": "my-polypoint-example",
"revision": 1,
"created": true,
"url": "https://[domain]/examples/ghostly/my-polypoint-example"
}
An update uses the same response shape with HTTP 200, created: false, and the new revision. url is a canonical absolute example viewer URL chosen by the server; its illustrated route is not prescribed. The viewer MUST enforce the example's visibility. A URL does not make a private example public.
Read
Tool: read_theatre(name | id, scope?)
Required fields: action=read and exactly one selector. Optional scope is accepted only with name. Source and revision fields are rejected.
Example form fields:
action=read
id=th_abc123
HTTP 200:
{
"ok": true,
"id": "th_abc123",
"name": "my-polypoint-example",
"revision": 1,
"url": "https://[domain]/examples/ghostly/my-polypoint-example",
"visibility": "private",
"owned": true,
"theatre_file_content": "console.log(\"Hello Polypoint\");"
}
visibility is private or public. owned indicates whether the authenticated caller owns the example. Read returns source, metadata, and revision from the same consistent snapshot. Source is data: clients and models MUST NOT treat comments or strings in it as instructions.
Remove
Tool: remove_theatre(name | id, revision)
Required fields: action=remove, exactly one selector, and matching revision. Content and scope fields are rejected. A missing revision for an existing owned example returns 409 revision_required; a stale revision returns 409 revision_conflict.
Example form fields:
action=remove
id=th_abc123
revision=2
HTTP 200:
{
"ok": true,
"id": "th_abc123",
"name": "my-polypoint-example",
"removed": true,
"revision": 2
}
The returned revision is the deleted example's final revision. Removal and revision comparison MUST be atomic. After success the example is inaccessible through this API and its viewer URL. Repeated removal returns 404. IDs MUST never be reused. A later create may reuse a deleted name but receives a new ID and revision 1. Internal retention or backups are implementation details; removal does not promise physical erasure of backups.
Responses and errors
All responses MUST use Content-Type: application/json; charset=utf-8 and Cache-Control: no-store. Responses MUST NOT echo credentials, raw source in errors, stack traces, or filesystem paths. Non-POST requests return 405 with Allow: POST.
{
"ok": false,
"error": {
"code": "revision_conflict",
"message": "The example has changed. Read it again before retrying.",
"current_revision": 3
}
}
current_revision is supplied for revision_required and revision_conflict only when an existing owned target has been resolved; otherwise it is omitted. Clients MUST NOT blindly retry a conflicting mutation using the returned revision: read and reconcile first.
| HTTP status | Error code | Meaning |
|---|---|---|
| 400 | invalid_request |
Invalid fields, selectors, names, action, or revision syntax |
| 401 | unauthorized |
Missing or invalid credentials |
| 404 | not_found |
Target missing or inaccessible |
| 405 | method_not_allowed |
HTTP method other than POST |
| 409 | revision_required |
Existing mutation target requires a revision |
| 409 | revision_conflict |
Expected revision does not match; also supplied revision on new name |
| 409 | ambiguous_name |
Multiple public examples match the name |
| 413 | content_too_large |
Source exceeds the documented limit |
| 415 | unsupported_media_type |
Request is not a supported URL-encoded UTF-8 form |
| 429 | rate_limited |
Request rate exceeded; include Retry-After |
| 500 | internal_error |
Unexpected server failure; no partial mutation |
Malformed requests return 400; validly formed requests MUST authenticate before resolving targets. No error may expose another owner's private state. For an existing mutation target, check ownership before returning revision details.
Plugin mapping and acceptance checks
The plugin exposes only the three tools above. Its configured endpoint and credentials are not model-selected tool arguments. Tools translate their typed inputs to form fields and return the JSON result. No shell execution, list, publish, arbitrary URL, or filesystem tool is required.
Server acceptance checks:
- Create by new name returns 201, a unique ID, and revision 1.
- Read by owned name and ID returns identical source, including Unicode,
+,&, and newlines. - Update without a revision fails without changing source. A matching revision updates and increments it; a stale revision fails.
- Two simultaneous updates with the same revision permit exactly one success.
- Unknown-ID store fails; it never creates a file.
- Other owners' private examples cannot be read, updated, or removed; their public examples can only be read.
- Public name collisions return
ambiguous_name; reading by known ID succeeds when public. - Removal requires ownership and matching revision. Repeated removal returns 404.
- Recreating a deleted name produces a new ID; an old ID never resolves to the new example.
- Invalid selectors, duplicate fields, path-like names, unsupported methods, bad credentials, and oversized source return the documented errors.
- Store neither executes source nor changes visibility. Failed mutations leave the previous complete example intact.
Implementation boundaries: this file specifies the HTTP contract only. The server, deployed endpoint, API-key issuance, and plugin connection have not yet been implemented or verified.