{"openapi":"3.1.0","info":{"title":"Pepperboard Agent API","version":"1.0.0","description":"Versioned agent surface for Pepperboard. Auth: Authorization: Bearer pb_live_… Send Idempotency-Key on every mutation; replays within 24h return the original response."},"servers":[{"url":"https://pepperboard-production.up.railway.app"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pb_live_…","description":"Either a legacy pb_live_… key, or a short-lived token from POST /api/v1/auth/token (Bearer)."},"sessionAuth":{"type":"apiKey","in":"cookie","name":"pb_session","description":"Dashboard session cookie (HttpOnly, set at login). Required for every /api/agent/* management endpoint."}},"schemas":{"Error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}},"SessionError":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string","example":"pick at least one scope"},"hint":{"type":"string"}},"description":"Error shape for the session-authenticated /api/agent/* endpoints (distinct from the v1 {code,message} shape)."},"ApiKeyV2":{"type":"object","required":["ok","id","name","client_key","client_secret","scopes","workspace_id"],"properties":{"ok":{"type":"boolean","example":true},"id":{"type":"string","format":"uuid","description":"Key id — used in rotate/revoke paths"},"name":{"type":"string"},"client_key":{"type":"string","pattern":"^pk_live_[A-Za-z0-9_-]{24}$","description":"Public client key — safe in config files"},"client_secret":{"type":"string","pattern":"^sk_live_[A-Za-z0-9_-]{43}$","description":"Secret — shown ONCE, never returned again"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"workspace_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"setup_prompt":{"type":"string","description":"Copy-paste connection kit for the agent"}}},"Workspace":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"Database-generated uuid"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"key_count":{"type":"integer","description":"Present on GET list only"}}},"Envelope":{"type":"object","required":["id","event","occurred_at","data"],"properties":{"id":{"type":"string","example":"evt_9f2c…"},"event":{"type":"string","enum":["instruction.created","instruction.completed","instruction.failed","video.pending_review","video.approved","video.rejected","idea.created","idea.research_ready","channel.scheduled","post.published","post.failed","connection.test"]},"occurred_at":{"type":"string","format":"date-time"},"data":{"type":"object"}}}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"},"description":"One UUID per logical operation; replay returns the original response for 24h."}}},"paths":{"/api/v1/auth/token":{"post":{"summary":"Exchange a client_key + client_secret pair for a short-lived access token","description":"Client-credentials exchange. Send the public client_key (pk_live_…) in the X-Client-Key header and the secret (sk_live_…) once in the JSON body. Returns a JWT valid for 1 hour; call /api/v1 with \"Authorization: Bearer <access_token>\".","parameters":[{"name":"X-Client-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^pk_live_[A-Za-z0-9_-]{24}$"},"description":"Public client key (pk_live_…)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_secret"],"properties":{"client_secret":{"type":"string","example":"sk_live_…"}}}}}},"responses":{"200":{"description":"{ access_token, token_type: \"Bearer\", expires_in: 3600, scopes }"},"401":{"description":"bad credentials (code: invalid_client)","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}},"429":{"description":"rate limited (code: rate_limited, Retry-After header)","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}},"503":{"description":"TOKEN_SIGNING_SECRET not configured (code: token_unavailable)","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}}}}},"/api/agent/keys/v2":{"post":{"summary":"Issue a client_key + client_secret pair for a workspace","description":"The client_secret is returned ONCE in this response — only its scrypt hash is stored. Save it immediately; it cannot be recovered (rotate if lost).","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scopes"],"properties":{"name":{"type":"string","maxLength":80,"default":"Untitled key"},"scopes":{"type":"array","minItems":1,"items":{"type":"string","enum":["board:read","instructions:read","instructions:write","videos:read","videos:write","approvals:read","ideas:read","ideas:write","media:read","webhooks:write","goals:read","goals:write","email:read","email:write","calendar:read","calendar:write","channels:read","channels:write","social:read","social:write"]},"example":["board:read","ideas:write","webhooks:write"]}}}}}},"responses":{"200":{"description":"key pair issued (secret shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyV2"}}}},"400":{"description":"pick at least one scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"404":{"description":"unknown_workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"503":{"description":"writes-not-configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}}},"/api/agent/keys/{id}/rotate":{"post":{"summary":"Rotate a v2 key secret (old secret + outstanding tokens die immediately)","description":"The new secret is returned ONCE. An explicit ?workspace_id= pins the lookup to that workspace; legacy pb_live_… keys cannot be rotated.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API key id (uuid)"},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional: pin the lookup to this workspace"}],"responses":{"200":{"description":"{ ok: true, id, client_key, client_secret (new, shown once) }","content":{"application/json":{"schema":{"type":"object","required":["ok","id","client_key","client_secret"],"properties":{"ok":{"type":"boolean","example":true},"id":{"type":"string","format":"uuid"},"client_key":{"type":"string"},"client_secret":{"type":"string","description":"New secret — shown once, never returned again"}}}}}},"404":{"description":"key not found — or a legacy pb_live_ key, which cannot be rotated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"503":{"description":"writes-not-configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}}},"/api/agent/keys/{id}/revoke":{"post":{"summary":"Revoke one key (outstanding tokens die on their next request)","description":"An explicit ?workspace_id= pins the lookup; a key id from another workspace then 404s and is never touched.","security":[{"sessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API key id (uuid)"},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional: pin the lookup to this workspace"}],"responses":{"200":{"description":"{ ok: true }","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true}}}}}},"404":{"description":"key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"502":{"description":"could not revoke key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}}},"/api/agent/workspaces/{workspaceId}/revoke-keys":{"post":{"summary":"Revoke EVERY key in a workspace","description":"Sets revoked=true on all unrevoked keys in the workspace; returns the count. Other workspaces are untouched.","security":[{"sessionAuth":[]}],"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string"},"description":"Workspace id"}],"responses":{"200":{"description":"{ ok: true, workspace_id, revoked: n }","content":{"application/json":{"schema":{"type":"object","required":["ok","workspace_id","revoked"],"properties":{"ok":{"type":"boolean","example":true},"workspace_id":{"type":"string"},"revoked":{"type":"integer","example":2}}}}}},"404":{"description":"unknown_workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"503":{"description":"writes-not-configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}}},"/api/agent/workspaces":{"get":{"summary":"List every workspace with its key count (secrets and hashes never leave)","security":[{"sessionAuth":[]}],"responses":{"200":{"description":"{ ok: true, workspaces: [...] }","content":{"application/json":{"schema":{"type":"object","required":["ok","workspaces"],"properties":{"ok":{"type":"boolean","example":true},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}}}}}}},"502":{"description":"could not list workspaces","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}},"post":{"summary":"Create a workspace (id is database-generated; the client cannot choose it)","security":[{"sessionAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80,"example":"Client sandbox"}}}}}},"responses":{"200":{"description":"{ ok: true, workspace: { id, name, created_at } }","content":{"application/json":{"schema":{"type":"object","required":["ok","workspace"],"properties":{"ok":{"type":"boolean","example":true},"workspace":{"$ref":"#/components/schemas/Workspace"}}}}}},"400":{"description":"invalid_name — name must be non-empty and at most 80 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}},"503":{"description":"writes-not-configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionError"}}}}}}},"/api/v1/board":{"get":{"summary":"Full board state","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"channels, videos, instructions"},"401":{"description":"bad key","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}}}}},"/api/v1/instructions":{"get":{"summary":"List instructions (filter ?status=new|picked_up|completed|failed)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"instruction list"}}}},"/api/v1/instructions/{id}/pickup":{"post":{"summary":"Claim an instruction (new → picked_up)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"picked up"},"409":{"description":"bad transition","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}}}}},"/api/v1/instructions/{id}/complete":{"post":{"summary":"Mark an instruction complete (picked_up → completed)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"completed"}}}},"/api/v1/instructions/{id}/fail":{"post":{"summary":"Mark an instruction failed (picked_up → failed)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"failed"}}}},"/api/v1/videos/{id}/decision":{"post":{"summary":"Approve or reject a pending video (pending → approved|rejected)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"decision saved"},"409":{"description":"bad transition","content":{"application/json":{"schema":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_key"},"message":{"type":"string"},"hint":{"type":"string"},"request_id":{"type":"string"}}}}}}}}},"/api/v1/videos/{id}/signed-urls":{"get":{"summary":"Short-lived (24h) signed media URLs","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"urls"}}}},"/api/v1/ideas":{"get":{"summary":"List channel ideas","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"idea list"}}},"post":{"summary":"Submit a new channel idea","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"idea created"}}}},"/api/v1/ideas/{id}/research":{"post":{"summary":"Submit the research package (→ package_ready)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"research saved"}}}},"/api/v1/inbox":{"get":{"summary":"Drain unclaimed Muse-inbox events","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"events"}}}},"/api/v1/inbox/claim":{"post":{"summary":"Claim inbox events {\"ids\":[…]}","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"claimed"}}}},"/api/v1/inbox/{id}/handled":{"post":{"summary":"Mark an inbox event handled","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"handled"}}}}}}