Integrations API
Every Topic in Peeved can issue its own API key. The key is scoped to a single
Topic and a chosen subset of permissions (read, create,
edit, delete). Use it to read entries, write new ones,
patch structured fields, send chat messages, or subscribe to live changes — all
without going through the iOS app.
1. Create an API key
- Open the Peeved iOS app and select the Topic you want to integrate with.
- Tap the Edit Topic icon in the header, then scroll to Integrations.
- Tap New integration. Give it a name (e.g. "My todo bot") and choose its scopes.
- Peeved generates a key prefixed with
pvi_. Copy it now — it's shown once and never again. If you lose it, delete the integration and create a new one.
Each integration is bound to one Topic. To act on multiple Topics, create multiple integrations. There's no shared / account-wide key by design — blast radius is always one Topic.
2. Authentication
Send the key as the X-API-Key request header. The base URL is:
https://interfacec-server.onrender.com
Quick sanity check:
curl -H "X-API-Key: pvi_your_key_here" \
https://interfacec-server.onrender.com/api/integrations/v1/whoami
{
"success": true,
"data": {
"integration_id": 1,
"thread_id": 334,
"name": "My todo bot",
"scopes": ["read", "create", "edit", "delete"]
}
}
3. Scopes
| Scope | Endpoints it unlocks |
|---|---|
read | List / get thread items, get the Topic schema, whoami |
create | POST a new thread item |
edit | PATCH an existing thread item |
delete | DELETE a thread item |
Pick the minimum set you need. You can rotate by deleting and re-creating the integration.
4. Rate limits
Limits are per-integration, sliding-window, in requests per minute. Exceeding a
limit returns HTTP 429 with a JSON error body.
| Operation | Limit |
|---|---|
| Read (list / get / whoami) | 30 / minute |
| Create | 3 / minute |
| Edit | 10 / minute |
| Delete | 10 / minute |
| Chat | 6 / minute |
5. Endpoints
GET /api/integrations/v1/whoami
Returns the integration's id, thread id, name, and scopes. Useful as a connectivity check.
GET /api/integrations/v1/thread
Returns the Topic this key is bound to — including its schema.columns, which is what you'll use to send valid datapoints. Example:
{
"success": true,
"data": {
"id": 334,
"name": "Software todo",
"description": "Task tracking and status",
"schema": {
"columns": [
{ "key": "taskName", "type": "string", "label": "Task Name" },
{ "key": "priority", "type": "picklist", "label": "Priority",
"picklistValues": [
{ "value": "High", "color": "#FF3B30" },
{ "value": "Medium", "color": "#FF9F0A" },
{ "value": "Low", "color": "#34C759" }
]
},
{ "key": "status", "type": "picklist", "label": "Status",
"picklistValues": [
{ "value": "Backlog", "color": "#8E8E93" },
{ "value": "In Progress", "color": "#30B0FF" },
{ "value": "Review", "color": "#AF52DE" },
{ "value": "Completed", "color": "#34C759" },
{ "value": "Ready for Dev","color": "#E5B800" }
]
},
{ "key": "estimate", "type": "double", "label": "Estimate", "units": "hrs" },
{ "key": "tags", "type": "array[string]", "label": "Tags" }
]
}
}
}
GET /api/integrations/v1/thread-items
Lists items, newest first. Optional query: ?limit=N (default 50, max 200).
GET /api/integrations/v1/thread-items/:itemId
Returns a single item. Response shape (same as list rows):
{
"success": true,
"data": {
"id": 3685,
"thread_id": 334,
"parent_id": null,
"type": "user",
"content": "Remove the trashcan fade-in animation from the main header",
"image_url": null,
"thumb_url": null,
"mood": 5,
"datapoints": {
"taskName": "Remove trashcan fade-in animation",
"priority": "Low",
"status": "In Progress",
"estimate": 1,
"tags": ["UI", "Header", "Frontend"]
},
"integration_id": 1,
"integration_name": "My todo bot",
"created_at": "2026-05-03T14:47:27.448Z",
"modified_at": "2026-05-03T15:12:08.001Z"
}
}
datapoints is always present and is always an object (possibly empty).
Keys match the Topic's schema.columns[].key.
POST /api/integrations/v1/thread-items
Create a new item. Optional body fields: content, image_url, thumb_url, mood, type, datapoints.
curl -X POST https://interfacec-server.onrender.com/api/integrations/v1/thread-items \
-H "X-API-Key: pvi_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"content": "Wire up onboarding analytics",
"datapoints": {
"taskName": "Wire up onboarding analytics",
"priority": "High",
"status": "Backlog",
"estimate": 3,
"tags": ["Analytics", "Onboarding"]
}
}'
PATCH /api/integrations/v1/thread-items/:itemId
Update one or more fields. Send only the keys you want to change — datapoints is merged with the existing values.
curl -X PATCH https://interfacec-server.onrender.com/api/integrations/v1/thread-items/3685 \
-H "X-API-Key: pvi_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "datapoints": { "status": "In Progress" } }'
DELETE /api/integrations/v1/thread-items/:itemId
Permanently removes the item. Returns { "success": true, "data": { "id": 3685 } }.
POST /api/integrations/v1/chat
Post a chat message to the Topic's chat thread. The author is the integration's
name (so users can tell the messages came from your bot, not them). Body:
{ "content": "..." }.
6. Datapoints — schema-aware writes
datapoints is the same on every Topic, regardless of schema — a flat
JSON object keyed by schema.columns[].key. The server validates each
value against its column type:
| Column type | Accepted JSON |
|---|---|
string / text / date | String |
double / integer | Number (or numeric string — coerced) |
boolean | true / false |
picklist | String matching one of picklistValues[].value |
array[string] | Array of strings |
Unknown column keys are rejected with 400. Picklist values that aren't
in the allowed list are rejected with 400 and a list of allowed values
in the error message.
On PATCH, datapoints is merged with the
existing record — so to flip one field (e.g. status), you don't need
to resend the others.
7. WebSocket — live updates
Every Peeved account has a WebSocket service that fans out item-level
events as they happen. Connect with the same API key you use for HTTP
calls and you'll receive every event for the user who owns the
integration's Topic — filter to your thread_id client-side
to ignore the rest.
The transport is socket.io (v4). Endpoint:
wss://interfacec-server-ws.onrender.com
Connecting
// Node.js — npm i socket.io-client
const { io } = require("socket.io-client");
const API_KEY = process.env.PEEVED_API_KEY; // pvi_...
const THREAD_ID = 334; // your integration's thread
const socket = io("https://interfacec-server-ws.onrender.com", {
transports: ["websocket"],
// socket.io-client only sends headers on the (optional) HTTP polling
// handshake. The Peeved WS server therefore also accepts the key as a
// query param so a websocket-only connection can authenticate.
extraHeaders: { "X-API-Key": API_KEY },
query: { apiKey: API_KEY },
});
socket.on("connect", () => console.log("connected", socket.id));
socket.on("connect_error", (err) => console.error("auth failed:", err.message));
socket.on("message", (raw) => {
const msg = typeof raw === "string" ? JSON.parse(raw) : raw;
if (msg.threadId !== THREAD_ID) return; // not for us
console.log(msg.type, msg.id, msg.data);
});
What you'll receive
The server emits a single event name — "message" — and
the payload is JSON with a type discriminator. The types
most useful to integrations:
| Type | When | Payload (key fields) |
|---|---|---|
integration_thread_item_new | An integration created an item via POST. | threadId, id, data (full row, including ai_response) |
threadItemQueue_* | Worker queued / completed processing for an item (analysis, schema generation, chart recs). | threadId, id, status, data |
threadQueue_* | Thread-level work (e.g. chart/metric recommendations) finished. | threadId, data |
The full DB row inside data uses the legacy iOS shape
(e.g. ai_response is a JSON string). If you want the
clean integration-API shape with parsed datapoints,
treat WS events as a notification and call
GET /api/integrations/v1/thread-items/:itemId
to read the canonical record.
Tips
- Filter by
threadIdclient-side. The server fans out the entire user feed (including non-integration activity) to every authenticated socket on that user's room. Drop anything whosethreadIdisn't yours. - Reconnect on drop. socket.io reconnects automatically; on reconnect, run a one-shot
GET /thread-items?limit=Nto backfill anything you missed. - De-duplicate by
id. WS pushes can arrive out of order with HTTP responses; using the highest-seenidper thread is the simplest reconciliation. - Heartbeats are automatic. Don't send your own ping — socket.io handles keep-alive.
- Auth is one-shot at handshake. If you delete the integration, existing sockets keep working until they disconnect; the next reconnect fails with "Authentication error".
8. Errors
Every error response is JSON with success: false and a human-readable message:
{ "success": false, "message": "\"status\" must be one of: Backlog, In Progress, Review, Completed, Ready for Dev" }
| Status | Meaning |
|---|---|
400 | Validation failed (bad column key, wrong type, picklist mismatch, no updatable fields). |
401 | Missing or invalid X-API-Key. |
403 | Key is valid but missing the required scope for the operation. |
404 | Item or thread not found, or doesn't belong to this integration. |
429 | Rate limit exceeded. Back off and retry. |
5xx | Server error. Safe to retry with backoff. |
9. Worked example — flip a todo to "In Progress"
# 1. List recent items
curl -H "X-API-Key: pvi_..." \
https://interfacec-server.onrender.com/api/integrations/v1/thread-items?limit=5
# 2. Pick one (id: 3685) and flip its status
curl -X PATCH https://interfacec-server.onrender.com/api/integrations/v1/thread-items/3685 \
-H "X-API-Key: pvi_..." \
-H "Content-Type: application/json" \
-d '{ "datapoints": { "status": "In Progress" } }'
# 3. Confirm the merge — other datapoints (priority, tags, ...) untouched
curl -H "X-API-Key: pvi_..." \
https://interfacec-server.onrender.com/api/integrations/v1/thread-items/3685
10. Notes
- Keys begin with
pvi_. They're shown to you exactly once; rotate by deleting and re-creating. - The server stores only a hash of the key, never the key itself.
- Integration writes show in the iOS app live (via WebSocket) the same as user-authored items, with the integration's name attached.
- Need a different rate limit, a webhook, or a feature you don't see here? Email info@massiveinterface.com.