Ideas API
Create, list, and manage LinkedIn post ideas. All routes require a Cognito JWT and use
snake_case request bodies (per the API-wide convention).
For the feature-level model (lifecycle, AI title generation, UI behavior), see
Idea Capture & Management. This page is the endpoint-by-endpoint
wire contract; the authoritative spec is specs/api/ideas.md in the repo.
Route summary
| Method | Path | Description |
|---|---|---|
POST | /api/ideas | Create an idea |
GET | /api/ideas?workspaceId= | List ideas |
GET | /api/ideas/{id}?workspaceId= | Get an idea |
PUT | /api/ideas/{id} | Update an idea |
DELETE | /api/ideas/{id}?workspaceId= | Delete an idea |
PUT | /api/ideas/{id}/status | Change status |
Authorization
Every route requires workspace membership: the caller must have a member record in
the workspace_id (body field on POST/PUT, workspaceId query param on
GET/DELETE), or the route returns 403 Not a workspace member. There is no
additional per-role check — member, editor, and owner can all do everything below.
Every idea object below also carries raw storage keys — pk, sk, gsi1pk,
gsi1sk, entity_type — because the backend returns the persisted DynamoDB item
as-is rather than a sanitized response DTO. They're included in the JSON responses
shown here for accuracy, but treat them as internal, not part of the contract.
Create
POST /api/ideasThe title field in the request body is not stored as the title. It's treated
as free-form idea content: the backend calls Bedrock to generate a short title from
it, and stores your original text as description. See AI title
generation.
Body
{
"workspace_id": "ws-1",
"title": "Just realized our onboarding flow loses 40% of users at step 3...",
"tags": ["growth", "onboarding"],
"source_url": "https://example.com/analytics-dashboard"
}tags and source_url are optional (default [] / "").
Response 201
{
"idea_id": "3f9e...",
"workspace_id": "ws-1",
"title": "Fixing Onboarding Drop-off",
"description": "Just realized our onboarding flow loses 40% of users at step 3...",
"tags": ["growth", "onboarding"],
"source_url": "https://example.com/analytics-dashboard",
"status": "new",
"created_by": "user-111",
"linked_post_id": null,
"created_at": "2026-07-17T12:00:00+00:00",
"updated_at": "2026-07-17T12:00:00+00:00"
}| Status | Meaning |
|---|---|
201 | Created |
400 | workspace_id missing/blank |
400 | title (content) missing/blank |
403 | Not a workspace member |
List
GET /api/ideas?workspaceId=ws-1&status=new&tags=growth,onboarding&authorId=user-111workspaceId is required; status, tags, and authorId are all optional filters
that combine with AND. Results are sorted newest-first by created_at.
Response 200
{ "items": [ /* idea objects, same shape as Create's response */ ], "count": 2 }| Status | Meaning |
|---|---|
200 | OK — count: 0 / items: [] if none match |
400 | workspaceId query param missing/blank |
403 | Not a workspace member |
Get
GET /api/ideas/{id}?workspaceId=ws-1Response 200 — the idea object (same shape as Create's response).
| Status | Meaning |
|---|---|
200 | OK |
400 | workspaceId query param missing/blank |
403 | Not a workspace member |
404 | Idea not found |
Update
PUT /api/ideas/{id}Partial update — only the fields you send are changed.
Body
{
"workspace_id": "ws-1",
"title": "Fixing Onboarding Drop-off (v2)",
"description": "Updated context...",
"source_url": "https://example.com/updated-link",
"tags": ["growth"]
}title, if present, is trimmed and must be 1–500 characters after trimming, or the request fails with400.description,source_url, andtagsare stored as-is with no validation.statusis not accepted here — use Change status instead.
Response 200 — the updated idea object.
| Status | Meaning |
|---|---|
200 | OK |
400 | workspace_id missing/blank |
400 | title present but blank, or over 500 characters |
403 | Not a workspace member |
404 | Idea not found |
Delete
DELETE /api/ideas/{id}?workspaceId=ws-1Hard-deletes the idea. No status restriction — a converted idea can be deleted too.
Response 200
{ "deleted": true }| Status | Meaning |
|---|---|
200 | Deleted |
400 | workspaceId query param missing/blank |
403 | Not a workspace member |
404 | Idea not found |
Update Status
PUT /api/ideas/{id}/statusTransitions an idea's status. The only allowed transition is new → converted.
Everything else — including new → new — is rejected.
Body
{ "workspace_id": "ws-1", "status": "converted" }Response 200 — the updated idea object, with status changed.
| Status | Meaning |
|---|---|
200 | Transitioned |
400 | workspace_id missing/blank |
400 | status missing/blank |
400 | status is not new or converted |
403 | Not a workspace member |
404 | Idea not found |
409 | Transition not allowed from the idea's current status (message: Transition from '{current}' to '{new}' is not allowed) |
In practice, most ideas reach converted automatically — as a side effect of
POST /api/posts when the new post is created with a source_idea_id pointing at
this idea. This endpoint exists for direct/automation use; the web app's "Generate
Post" flow never calls it directly. See Idea lifecycle.
See also
- Idea Capture & Management — product behavior and lifecycle.
- MCP → Ideas Tools — the AI-agent surface, which behaves differently from this REST API in several places (no title generation, no status-transition validation).
- Posts API — creating a post from an idea.