Examples API
Full REST reference for the examples domain. All routes are under
/api/examples, require a Cognito JWT (Authorization: Bearer <token>),
and are backed by services/backend/lambda/examples_handler.py /
examples_service.py. See specs/api/examples.md in the repo for the
underlying spec this page is based on, and the
Examples feature guide for the product-level
behavior.
Request bodies are snake_case per lk-wiz convention (workspace_id,
performance_notes, ...). Query parameters on this domain are camelCase
(workspaceId) — the standard lk-wiz split (bodies snake_case, query
params camelCase).
custom:role is read from the JWT on every route but is never checked —
this domain enforces workspace membership only, not role. A viewer
can create, edit, and delete examples exactly like an editor or owner.
Endpoints at a glance
| Method | Path | Description |
|---|---|---|
POST | /api/examples | Create an example |
GET | /api/examples | List examples |
GET | /api/examples/{id} | Get an example |
PUT | /api/examples/{id} | Update an example |
DELETE | /api/examples/{id} | Delete an example |
POST | /api/examples/import-from-post/{id} | Import a published post as an example |
Create
POST /api/examples// Request
{
"workspace_id": "ws_123",
"title": "How I closed my first enterprise deal",
"content": "Full post text here...",
"tags": ["sales", "storytelling"],
"performance_notes": "50K impressions, 200 comments"
}// 201 Response
{
"example_id": "e1e2e3e4-...",
"workspace_id": "ws_123",
"title": "How I closed my first enterprise deal",
"content": "Full post text here...",
"tags": ["sales", "storytelling"],
"performance_notes": "50K impressions, 200 comments",
"created_by": "user_111",
"created_at": "2026-07-17T12:00:00+00:00",
"updated_at": "2026-07-17T12:00:00+00:00"
}workspace_id, title, and content are required (400 if missing or
empty after trimming). Validation runs in that order, so an empty title
fails before workspace membership is even checked. tags defaults to [],
performance_notes to "".
| Status | Reason |
|---|---|
| 400 | Missing/empty workspace_id, title, or content |
| 403 | Caller is not a member of workspace_id |
List
GET /api/examples?workspaceId=ws_123// 200 Response
{ "items": [ /* example objects */ ], "count": 3 }No pagination, tag filter, or sort parameter exists on this endpoint — it's
a single DynamoDB GSI1 query returning every example in the workspace in
ascending creation-time order (oldest first). There's no ?sort= to
reverse that, and a workspace with enough examples to exceed one DynamoDB
query page (1 MB) would see results silently truncated.
| Status | Reason |
|---|---|
| 400 | Missing workspaceId |
| 403 | Caller is not a member of workspaceId |
Get
GET /api/examples/{id}?workspaceId=ws_123Returns the example object (same shape as Create's response).
| Status | Reason |
|---|---|
| 400 | Missing workspaceId |
| 403 | Caller is not a member of workspaceId |
| 404 | No example {id} in that workspace |
Update
PUT /api/examples/{id}Partial update — send only the fields you want to change, plus
workspace_id (required for the membership check, not itself persisted as
a change).
// Request — retag and update performance notes
{ "workspace_id": "ws_123", "tags": ["sales", "closing"], "performance_notes": "Now at 60K impressions" }| Field | Behavior |
|---|---|
title | coerced to string, stripped, replaces title — no non-emptiness check on update, unlike Create; sending "" clears it |
content | coerced to string, stripped, replaces content — same no-emptiness-check caveat |
tags | replaces tags as-is, no validation |
performance_notes | replaces performance_notes as-is |
updated_at is always refreshed, even for a body containing no recognized
fields. source_post_id (present only on imported examples) cannot be
changed through this endpoint.
| Status | Reason |
|---|---|
| 400 | Missing workspace_id |
| 403 | Caller is not a member of workspace_id |
| 404 | Example not found |
Delete
DELETE /api/examples/{id}?workspaceId=ws_123Any workspace member may delete any example — there is no creator-only or
admin-only restriction on this domain (contrast with actions, where
delete is creator/admin-only).
| Status | Reason |
|---|---|
| 200 | Deleted — { "deleted": true } |
| 400 | Missing workspaceId |
| 403 | Caller is not a member of workspaceId |
| 404 | Example not found |
Import from post
POST /api/examples/import-from-post/{id}{id} in the path is a post id, not an example id.
// Request
{ "workspace_id": "ws_123" }// 201 Response
{
"example_id": "e5e6e7e8-...",
"workspace_id": "ws_123",
"title": "Why we rebuilt our onboarding flow",
"content": "Full post text here...",
"tags": ["product"],
"performance_notes": "",
"source_post_id": "post_789",
"created_by": "user_111",
"created_at": "2026-07-17T12:00:00+00:00",
"updated_at": "2026-07-17T12:00:00+00:00"
}The source post must exist and have status: "published" — anything else
(draft, in review, scheduled) is rejected with 409. title, content,
and tags are copied from the post; performance_notes always starts
empty (never copied from the post, even if the post has performance data
recorded elsewhere); source_post_id is set to the post's id and is the
one field that distinguishes an imported example from a manually-created
one.
| Status | Reason |
|---|---|
| 400 | Missing workspace_id |
| 403 | Caller is not a member of workspace_id |
| 404 | No post {id} in that workspace |
| 409 | Post exists but is not published |