Skip to content

REST API

Patchrooms has a small REST API for reading and updating the data behind a single project — projects, feedback reports, channels, and API keys. It is intentionally minimal: most teams pull feedback through the MCP endpoint or the dashboard, and use this API for custom integrations and for agent self-setup.

https://room.patchrooms.com/api/v1

Every route lives under a project and is authenticated with a secret API key (prefix pr_sk_) created in the dashboard, sent as a Bearer token bound to that project:

Authorization: Bearer pr_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Each endpoint requires a specific scope (listed below). Scopes are feedback:read|write, channel:read|write, project:read|write, and apikey:write. A short-lived setup token carries * (satisfies any scope) and may set a TTL — see Agent self-setup. A missing, revoked, or expired key returns 401; a key without the required scope returns 403.

In the paths below, :projectId is the project’s internal id (a MongoDB ObjectId), not the public pr_ project key.

GET /api/v1/me

Resolves the project from the key itself — no :projectId needed. Lets an agent bootstrap from the key alone (the same project the MCP endpoint resolves). Works with any scope.

{
"keyId": "665a...",
"projectId": "665e...",
"name": "Acme App",
"projectKey": "pr_xxx",
"scopes": ["feedback:read", "feedback:write"]
}
GET /api/v1/projects/:projectId

Scope: project:read. Returns the project’s name, public key, URL patterns, default channel, and whether an identity secret is set.

{
"id": "665e...",
"name": "Acme App",
"projectKey": "pr_xxx",
"urlPatterns": ["https://app.acme.com/*"],
"defaultChannelKey": "bug",
"identitySecretSet": false,
"createdAt": "2026-05-01T12:00:00.000Z"
}
PATCH /api/v1/projects/:projectId

Scope: project:write. Updates any of name, urlPatterns, defaultChannelKey, and widgetConfig. widgetConfig, when present, is a full replacement (mascot, shape, anchor, offsetX, offsetY, size, triggerVariant). Returns the updated project.

GET /api/v1/projects/:projectId/feedback

Scope: feedback:read. Returns reports newest first with cursor pagination.

Query paramTypeDescription
limitnumberPage size. Defaults to 20, capped at 100.
channelKeystringFilter by channel key.
statusstringFilter by report status.
beforestringCursor — a report id; returns reports older than it.
{
"items": [
{
"id": "665f...",
"channelKey": "bug",
"status": "open",
"blocks": [ /* Block[] */ ],
"context": { /* ... */ },
"identity": null,
"createdAt": "2026-06-03T09:14:22.000Z"
}
],
"nextCursor": "665e..."
}

Pass nextCursor back as the before query param to fetch the next page. nextCursor is null on the last page.

GET /api/v1/projects/:projectId/feedback/:reportId

Scope: feedback:read. Returns one report with its full blocks, context, and identity. Returns 404 if the report is not found in this project.

PATCH /api/v1/projects/:projectId/feedback/:reportId

Scope: feedback:write. Body: { "status": "<status>" }. An invalid status returns 400.

{ "id": "665f...", "status": "resolved" }

A room groups reports under one artifact (artifactId). Rooms exist implicitly the moment a report carries an artifactId — these endpoints let you formalize one (give it a title/goal, or archive it) with or without an existing report. POST is an idempotent upsert keyed on artifactId, so an agent can call it every time it starts a task without checking first.

GET /api/v1/projects/:projectId/rooms

Scope: feedback:read. Returns Room docs unioned with the implicit rooms derived from report artifactIds, sorted by last activity. A room with no reports yet still appears (count: 0). Pass ?archived=true to include archived rooms.

{
"items": [
{
"artifactId": "checkout-redesign",
"title": "Checkout redesign",
"meta": { "tool": "lovable", "goal": "Ship the new checkout flow", "url": null, "source": null, "constraints": null },
"archived": false,
"lastActivity": "2026-07-25T09:14:22.000Z",
"count": 3
}
]
}
POST /api/v1/projects/:projectId/rooms

Scope: feedback:write. Idempotent upsert by artifactId — safe to call every time you begin work on an artifact, before any report exists. Returns 201 on the call that creates the room, 200 on a repeat call; either way the body includes created so you can tell them apart.

Body fieldTypeDescription
artifactIdstringRequired. Stable id for the artifact/task.
titlestringRoom title. Defaults to artifactId.
metaobject{ tool?, source?, goal?, constraints?, url? }.
{ "id": "665f...", "artifactId": "checkout-redesign", "title": "Checkout redesign", "meta": null, "archived": false, "created": true }
PATCH /api/v1/projects/:projectId/rooms/:artifactId

Scope: feedback:write. Body: any of { title?, meta?, archived? }. Returns 404 if no room with that artifactId exists yet — this endpoint updates, it doesn’t upsert (use POST for that).

GET /api/v1/projects/:projectId/channels

Scope: channel:read. Returns the project’s channels. Pass ?archived=true to include archived channels.

{
"items": [
{
"id": "665d...",
"key": "bug",
"name": "Bug",
"color": "#c0392b",
"mascot": "fox",
"baseWeight": 1,
"archived": false
}
]
}
POST /api/v1/projects/:projectId/channels

Scope: channel:write. Creates a channel from the request body. A duplicate key in the same project returns 409; invalid input returns 400.

PATCH /api/v1/projects/:projectId/channels/:channelKey

Scope: channel:write. Updates the matching channel. Returns 404 if no channel with that key exists in the project.

POST /api/v1/projects/:projectId/hints

Scope: channel:write. Records a short-lived hint that biases channel matching toward a channel (it expires after 5 minutes).

Body fieldTypeDescription
channelKeystringChannel to bias toward. Required.
weight'suggest' | 'max' | numberStrength of the hint. Required.
userIdstringOptional user this hint applies to.
{ "ok": true }

Mint and revoke keys from a key that carries apikey:write — typically a short-lived setup token during agent self-setup.

POST /api/v1/projects/:projectId/keys

Scope: apikey:write. Body: { "name", "scopes": [...], "expiresInSeconds"? }. The plaintext key is returned once and never again. Scopes must be a subset of the project scopes — * and apikey:write cannot be minted via the API (no self-replication or privilege escalation), which returns 400.

{
"id": "665a...",
"key": "pr_sk_...",
"name": "feedback agent",
"scopes": ["feedback:read", "feedback:write"],
"expiresAt": null
}
DELETE /api/v1/projects/:projectId/keys/:keyId

Scope: apikey:write. Revokes the key by id (a setup token can revoke itself at the end of setup). Returns 404 if no active key with that id exists in the project.

{ "id": "665a...", "revokedAt": "2026-06-29T13:49:31.334Z" }

Errors are returned as JSON with an error message and a code:

{ "error": "Not found", "code": "NOT_FOUND" }
HTTPcodeMeaning
400BAD_REQUESTInvalid input.
401UNAUTHORIZEDMissing, invalid, or revoked API key.
403FORBIDDENKey lacks the required scope.
404NOT_FOUNDResource not found in this project.
409CONFLICTChannel key already exists.