Amba

Content Libraries

Manage and schedule content delivery — daily tips, quotes, articles, and more.

Content libraries let you manage collections of content items (tips, quotes, affirmations, articles) and deliver them to users on a schedule. Great for apps that need daily content rotation.

Content library or collection? A content library is a global published catalog — every user reads the same items. A collection defaults to per-user (read_policy: "owner"): each user sees only rows they own, so catalog rows you seed through the admin API are invisible to end users — the most common migration trap. If your catalog needs a typed schema and the filter DSL, create the collection with read_policy: "public" instead, so every signed-in user can read it. Decide before you seed.

How it works

  1. Create a content library (e.g., "Daily Motivation", "Workout Tips")
  2. Add content items to the library with text, media, categories, and tags
  3. Create a delivery schedule to control when and how items are delivered
  4. The app fetches today's content with the SDK and browses the full library through the client API

SDK usage

Get today's content

// Channel defaults to "default" — single-channel apps can call this bare.
const today = await Amba.content.today();
 
// Multi-channel apps pass the schedule's name explicitly:
const lessonsToday = await Amba.content.today('lessons');

The channel is the name of a delivery schedule. today returns a single ContentItem | null — the one item that schedule delivered for the current day window. It also returns null when a schedule set to on_exhausted: "stop" has delivered every item in its library; see Running out of content.

Browse a library

:libraryId takes the library's name or id. The response carries total, offset and limit next to data:

GET /v1/client/content/libraries/:libraryId?limit=20&offset=20

await Amba.content.library('Daily Tips', { limit: 20 }) returns the first page of active items as an array. Each item is a ContentItem: channel is the library's name, data the item's metadata, and published_at when the item was created; the item's columns (body, tags, category, media_url and the rest) come along with them.

Get a single item

GET /v1/client/content/items/:itemId

await Amba.content.item(itemId) returns the item in the same shape.

Create and update items from the client

The client API takes writes for user-authored content. The server stamps owner_app_user_id from the current session, and the caller cannot set it. An update returns 404 when the target item isn't owned by the current user.

Every user of the project reads every active item in a library, including items other users wrote. Use client writes for content meant to be shared, such as community tips. Keep private per-user data (todos, notes, drafts) in an owner-scoped collection.

POST /v1/client/content/libraries/Community%20Tips/items
 
{ "title": "Morning walk", "body": "A ten-minute walk before breakfast sets up the whole day.", "tags": ["habits"] }

body is required and must be a non-empty string; title, category, tags, metadata and media_url are optional. Update with PATCH /v1/client/content/items/:itemId; any field you pass overwrites, and { "is_active": false } hides the item from library reads while keeping the row for audit.

Amba.content.createItem('Community Tips', input) and Amba.content.updateItem(itemId, patch) send these requests and return the item.

The SDK has no delete method. DELETE /v1/client/content/items/:itemId removes an item the current user owns, and the admin API removes any item.

CreateContentItemInput:

interface CreateContentItemInput {
  title?: string | null;
  body: string;
  media_url?: string | null;
  category?: string | null;
  tags?: string[];
  metadata?: Record<string, unknown>;
}

UpdateContentItemInput:

interface UpdateContentItemInput {
  title?: string | null;
  body?: string;
  media_url?: string | null;
  category?: string | null;
  tags?: string[];
  metadata?: Record<string, unknown>;
  is_active?: boolean;
}

Client API reference

MethodPathDescription
GET/v1/client/content/today/:channelThe item the schedule named :channel delivered today.
GET/v1/client/content/todayEvery delivery recorded today, across schedules.
GET/v1/client/content/libraries/:idList items in a library by name or id (query: category, limit, offset).
GET/v1/client/content/items/:idFetch a single item.
POST/v1/client/content/libraries/:id/itemsCreate an item owned by the current user.
PATCH/v1/client/content/items/:idUpdate an item owned by the current user.
DELETE/v1/client/content/items/:idDelete an item owned by the current user.

Admin API reference

Libraries

MethodPathDescription
POST/v1/admin/projects/:projectId/content/librariesCreate library
GET/v1/admin/projects/:projectId/content/librariesList libraries with item counts
DELETE/v1/admin/projects/:projectId/content/libraries/:idDelete a library

Items

MethodPathDescription
POST/v1/admin/projects/:projectId/content/libraries/:id/itemsAdd items to library
GET/v1/admin/projects/:projectId/content/libraries/:id/itemsList items (paginated)
PATCH/v1/admin/projects/:projectId/content/items/:idUpdate an item
DELETE/v1/admin/projects/:projectId/content/items/:idDelete an item
POST/v1/admin/projects/:projectId/content/libraries/:id/bulkBulk import items

Schedules

MethodPathDescription
POST/v1/admin/projects/:projectId/content/schedulesCreate delivery schedule
GET/v1/admin/projects/:projectId/content/schedulesList schedules
PATCH/v1/admin/projects/:projectId/content/schedules/:idUpdate schedule
DELETE/v1/admin/projects/:projectId/content/schedules/:idDelete schedule

POST /v1/admin/projects/:projectId/content/libraries/:id/items

Body:

{
  "items": [
    {
      "title": "Stay Consistent",
      "body": "The key to progress isn't perfection — it's consistency.",
      "category": "motivation",
      "tags": ["daily", "mindset"],
      "is_premium": false
    }
  ]
}

Schedule types

TypeDescription
daily_rotationRotates through items one per day
weeklyDelivers one item per week
randomPicks a random item each delivery, each item once per pass
sequentialDelivers items in sort order

Every schedule reports its runway (items_remaining, runway_days) and raises content.library_running_low while fewer than 30 days of new content are left, so you can add items before players reach the end. See Content runway.

MCP tools

ToolDescription
amba_content_libraries_createCreate a content library
amba_content_items_addAdd items to a library
amba_content_schedules_createSet up a delivery schedule
amba_content_list_schedulesList schedules with their runway
amba_content_update_scheduleChange a schedule, its on_exhausted or its alarm

Example: Daily affirmations

Agent: "Set up a daily affirmation library with 5 items"

1. amba_content_libraries_create({ name: "Daily Affirmations", description: "Positive affirmations delivered daily" })
2. amba_content_items_add({ library_id: "<id from step 1>", items: [
     { body: "I am capable of achieving my goals." },
     { body: "Today I choose joy and gratitude." },
     { body: "I am getting stronger every day." },
     { body: "I deserve success and happiness." },
     { body: "Every day is a fresh start." }
   ]})
3. amba_content_schedules_create({ library_id: "<id from step 1>", name: "Morning Affirmation", schedule_type: "daily_rotation" })

Versioning + rollouts

Each content_item row carries three columns that together let you ship copy changes without an app deploy:

  • content_key — a stable, user-facing key (e.g. mission_intro, paywall_headline). Multiple rows with the same (library_id, content_key) are treated as versions of each other.
  • version — integer, immutable per row. The first POST under a key seeds version 1; subsequent POSTs auto-increment.
  • rollout_percent — integer in [0, 100]. The server hashes (user_id, item_id) into a [0, 99] bucket and serves the highest-version row whose bucket < rollout_percent. If no version matches, the highest-version row pinned at 100% serves as the stable baseline so every user always gets a row.

The hash is deterministic, so the same user always sees the same version on subsequent requests until you change rollout_percent. Distribution is uniform — at 50% rollout, expect within ±10% of half your users to land on the new version.

Items with content_key = NULL predate this feature and continue to behave exactly as before.

Ramp a rollout: 10% → 50% → 100%

The partner-shipped recipe for ramping a copy change with no app deploy:

# 1. Seed v1 — first POST under a fresh key. Auto-seeds at rollout_percent=100.
curl -X POST -H "Authorization: Bearer $DEV_TOKEN" \
  https://api.amba.dev/v1/admin/projects/$PROJECT/content/libraries/$LIB/items/mission_intro/versions \
  -d '{ "body": "Welcome to your mission. Tap to begin." }'
 
# 2. Author v2 — second POST under the same key. Lands at rollout_percent=0
#    (invisible to everyone) so you can review before exposing it.
curl -X POST -H "Authorization: Bearer $DEV_TOKEN" \
  https://api.amba.dev/v1/admin/projects/$PROJECT/content/libraries/$LIB/items/mission_intro/versions \
  -d '{ "body": "Welcome — your first mission is ready. Lets go." }'
 
# 3. Ramp v2: 0 → 10 → 50 → 100. Each PATCH is atomic; users re-bucket
#    deterministically so monitoring is straightforward.
curl -X PATCH -H "Authorization: Bearer $DEV_TOKEN" \
  https://api.amba.dev/v1/admin/projects/$PROJECT/content/libraries/$LIB/items/mission_intro/versions/2 \
  -d '{ "rollout_percent": 10 }'
# … review metrics …
curl -X PATCH .../mission_intro/versions/2 -d '{ "rollout_percent": 50 }'
# … review metrics …
curl -X PATCH .../mission_intro/versions/2 -d '{ "rollout_percent": 100 }'
 
# 4. (Optional) Demote v1 once v2 has been at 100% for a stable period.
curl -X PATCH .../mission_intro/versions/1 -d '{ "rollout_percent": 0 }'
 
# 5. Inspect rollout state at any time.
curl -H "Authorization: Bearer $DEV_TOKEN" \
  https://api.amba.dev/v1/admin/projects/$PROJECT/content/libraries/$LIB/items/mission_intro/versions
# → { "data": [{"version":2,"rollout_percent":100,...},{"version":1,"rollout_percent":0,...}], "total": 2 }

Versioning admin endpoints

MethodPathDescription
POST/v1/admin/projects/:projectId/content/libraries/:id/items/:key/versionsCreate a new version. First version of a key seeds at 100%; subsequent at 0%.
PATCH/v1/admin/projects/:projectId/content/libraries/:id/items/:key/versions/:vUpdate body / metadata / rollout_percent (0–100). Common: ramp the rollout.
GET/v1/admin/projects/:projectId/content/libraries/:id/items/:key/versionsList all versions of a key (DESC by version) with their rollout_percent each.

Versioned client fetch

GET /v1/client/content/libraries/:libraryId/keys/:contentKey

Returns the version the current authenticated user should see. The bucket is computed from the user's id, so two different users may receive different versions of the same key. The response shape matches the regular GET /v1/client/content/items/:id (a single ContentItem), so the SDK's rendering pipeline does not need special-casing.

If no version's bucket matches and no 100% baseline exists (the operator-error case where every version is at 0%), the highest-version row is returned anyway — better to serve something than nothing while the operator notices.