Amba

Content

Content libraries, items, bulk import, and scheduled delivery via scheduled workflows.

Content is organized as libraries (groupings) containing items (the actual content rows). Schedules run on scheduled workflows — one schedule row owns exactly one scheduled workflow under the deterministic id content-schedule-<row-id>. The CONTENT_DELIVERY workflow picks up the next item on each tick.

Endpoints

MethodPathDescription
POST/admin/projects/:projectId/content/librariesCreate a library.
GET/admin/projects/:projectId/content/librariesList libraries with item count.
DELETE/admin/projects/:projectId/content/libraries/:libraryIdDelete a library (?cascade=true to delete its items too).
POST/admin/projects/:projectId/content/libraries/:libraryId/itemsInsert items (serialized via advisory lock for stable sort_order).
GET/admin/projects/:projectId/content/libraries/:libraryId/itemsPaginated item list.
PATCH/admin/projects/:projectId/content/items/:itemIdPartial update of a single item.
DELETE/admin/projects/:projectId/content/items/:itemIdHard-delete an item.
POST/admin/projects/:projectId/content/libraries/:libraryId/bulkBulk import (id-only response).
POST/admin/projects/:projectId/content/schedulesCreate a schedule (validates cron first, then creates scheduled workflow).
GET/admin/projects/:projectId/content/schedulesList schedules with library name.
PATCH/admin/projects/:projectId/content/schedules/:scheduleIdUpdate a schedule (serialized under row lock; rollback-aware).
DELETE/admin/projects/:projectId/content/schedules/:scheduleIdDelete a schedule + its scheduled workflow handle.

POST /admin/projects/:projectId/content/libraries

Request

FieldTypeRequired
namestringyes
descriptionstringno

Response 201

{
  "data": { "id": "…", "name": "…", "description": null, "content_schema": null, "created_at": "…" }
}

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/content/libraries
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Daily tips"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Daily tips"}'

GET /admin/projects/:projectId/content/libraries

Response 200

{ "data": [{ "id": "…", "name": "…", "item_count": 42 }] }

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/content/libraries
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

DELETE /admin/projects/:projectId/content/libraries/:libraryId

Delete a content library. By default this refuses to delete a library that still has items (409 LIBRARY_NOT_EMPTY), so you don't silently lose content. Pass ?cascade=true to delete the library and all of its items, schedules, and deliveries in one atomic call — the cascade is irreversible. Use this to clean up orphan libraries instead of deleting items one at a time.

Query

ParamTypeDefaultDescription
cascadebooleanfalseWhen true, delete the library and all its items, schedules, and deliveries atomically. Irreversible.

Response 200

{ "data": { "id": "…", "deleted_item_count": 42 } }

deleted_item_count is the number of items removed (0 when deleting an empty library, or when cascade wasn't needed).

Errors

  • 409 LIBRARY_NOT_EMPTY — the library still has items and cascade=true was not passed. The error payload includes the current item count so you can decide whether to cascade.
  • 404 NOT_FOUND — no library with that id in this project.

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

# Refuses with 409 LIBRARY_NOT_EMPTY if the library still has items
curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries/{libraryId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"
 
# Cascade: delete the library and all its items, schedules, and deliveries
curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries/{libraryId}?cascade=true" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

The MCP tool amba_content_libraries_delete({ project_id, library_id, cascade }) wraps this endpoint for agentic use.

POST /admin/projects/:projectId/content/libraries/:libraryId/items

Insert one or more items. sort_order is computed from MAX+1 inside a per-library advisory lock so concurrent POSTs never collide.

Request

{ "items": [{ "body": "…", "title": null, "tags": [], "metadata": {}, "is_premium": false }] }

Per-item fields (from CreateContentItemInput):

FieldTypeRequired
titlestringno
bodystringyes
media_urlstringno
categorystringno
tagsstring[]no
metadataobjectno
is_premiumbooleanno

Response 201

{
  "data": [
    { "id": "…", "library_id": "…", "title": "…", "body": "…", "sort_order": 0, "is_active": true }
  ],
  "total": 1
}

Errors

  • 500 CREATE_FAILED.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/items
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/items' \
  -H 'Content-Type: application/json' \
  -d '{
  "items": [
    {
      "title": "Hydrate",
      "body": "Drink a glass of water."
    }
  ]
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries/{libraryId}/items" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"title":"Hydrate","body":"Drink a glass of water."}]}'

GET /admin/projects/:projectId/content/libraries/:libraryId/items

Paginated, is_active = true only.

Query

ParamDefault
limit50
offset0

Response 200

{ "data": [ … ], "total": 123, "offset": 0, "limit": 50 }

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/items
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/items'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries/{libraryId}/items" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

PATCH /admin/projects/:projectId/content/items/:itemId

Partial update. Allowed fields: title, body, media_url, category, tags, metadata, is_premium, is_active, sort_order.

Response 200

Updated row.

Errors

  • 404 NOT_FOUND.
  • 500 UPDATE_FAILED.

Try it:

PATCH/admin/projects/%7B%7BprojectId%7D%7D/content/items/%7B%7BitemId%7D%7D
developer auth
curl -X PATCH 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/items/%7B%7BitemId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X PATCH "${BASE_URL}/admin/projects/${PROJECT_ID}/content/items/{itemId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{}'

DELETE /admin/projects/:projectId/content/items/:itemId

Response 200

{ "data": { "deleted": true } }

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/content/items/%7B%7BitemId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/items/%7B%7BitemId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/content/items/{itemId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

POST /admin/projects/:projectId/content/libraries/:libraryId/bulk

Same sort-order behavior as the items POST, but returns only the count.

Response 201

{ "data": { "imported": 123 } }

Errors

  • 500 BULK_FAILED.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/bulk
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/libraries/%7B%7BlibraryId%7D%7D/bulk' \
  -H 'Content-Type: application/json' \
  -d '{
  "items": [
    {
      "body": "Drink a glass of water."
    },
    {
      "body": "Take a short walk."
    }
  ]
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/content/libraries/{libraryId}/bulk" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"body":"Drink a glass of water."},{"body":"Take a short walk."}]}'

POST /admin/projects/:projectId/content/schedules

Create a schedule and its backing scheduled workflow in a single tx. A cron field in config always wins; otherwise daily_rotation defaults to 0 9 * * *, weekly defaults to 0 9 * * MON, and random / sequential require an explicit cron.

Request

FieldTypeRequiredDescription
library_iduuidyesSource library.
namestringyesDisplay name.
schedule_type"daily_rotation" | "weekly" | "random" | "sequential"yes
config.cronstringconditionalOverrides defaults; required for random/sequential.
config.timezonestringnoIANA zone; defaults to "UTC".
batch_sizeinteger 1–100noItems per delivery. Defaults to 1.
on_exhausted"wrap" | "stop"noWhat happens once every item has been delivered. Defaults to "stop". See Running out of content.
runway_alarm_daysinteger 1–3650nocontent.library_running_low fires while the runway is below this many days. Defaults to 30.

Response 201

{
  "data": {
    "id": "…",
    "library_id": "…",
    "name": "…",
    "schedule_type": "daily_rotation",
    "config": { "cron": "0 9 * * *", "timezone": "UTC" },
    "is_active": true,
    "config_key": "content_<uuid>",
    "schedule_id": "content-schedule-…",
    "batch_size": 1,
    "on_exhausted": "stop",
    "runway_alarm_days": 30,
    "exhausted_at": null,
    "items_remaining": 120,
    "runway_days": 120,
    "items_per_day": 1,
    "exhausted": false
  }
}

Every schedule read (create, list, update) carries the runway fields:

FieldDescription
items_remainingItems the schedule has not delivered yet in its current pass through the library.
items_per_dayDeliveries per day at the schedule's cron × batch_size.
runway_daysitems_remaining ÷ items_per_day. null when the cron uses syntax the runway cannot read (six-field crons, L, W, #).
exhaustedtrue while a stop schedule has run out and delivers nothing.

Errors

  • 400 INVALID_SCHEDULE_CONFIG — bad cron or missing required cron.
  • 400 INVALID_BATCH_SIZE — batch_size outside 1–100.
  • 400 INVALID_ON_EXHAUSTED — on_exhausted is something other than "wrap" or "stop".
  • 400 INVALID_RUNWAY_ALARM_DAYS — runway_alarm_days outside 1–3650.
  • 409 CONTENT_RUNWAY_NOT_READY — the project is mid-upgrade and cannot store runway settings yet; retry in a few minutes.
  • 500 CREATE_FAILED.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/content/schedules
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/schedules' \
  -H 'Content-Type: application/json' \
  -d '{
  "library_id": "<library_id>",
  "name": "Daily rotation",
  "schedule_type": "daily_rotation"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/content/schedules" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"library_id":"<library_id>","name":"Daily rotation","schedule_type":"daily_rotation"}'

GET /admin/projects/:projectId/content/schedules

Response 200

{
  "data": [
    {
      "id": "…",
      "library_name": "…",
      "schedule_type": "daily_rotation",
      "config": { "timezone": "UTC", "last_index": 41 },
      "is_active": true,
      "config_key": "content_…",
      "schedule_id": "…",
      "batch_size": 1,
      "on_exhausted": "stop",
      "runway_alarm_days": 30,
      "items_remaining": 18,
      "runway_days": 18,
      "items_per_day": 1,
      "exhausted": false
    }
  ]
}

runway_days below runway_alarm_days means the schedule is firing content.library_running_low once a day; add items to the library to extend it.

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/content/schedules
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/schedules'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/content/schedules" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

PATCH /admin/projects/:projectId/content/schedules/:scheduleId

Update name, schedule_type, config, is_active, batch_size, on_exhausted, and/or runway_alarm_days. Row is locked FOR UPDATE so concurrent PATCHes serialize; workflow mutations are compensated if the DB commit later fails.

A config you send replaces the stored one and keeps the schedule's place in its library (last_index, last_delivered_item_id) unless you set those keys yourself, so changing the cron or timezone continues the schedule from where it was.

Request

FieldType
namestring
schedule_typestring
configobject
is_activeboolean
segment_iduuid | null
batch_sizeinteger 1–100
on_exhausted"wrap" | "stop"
runway_alarm_daysinteger 1–3650

Response 200

Updated row with its runway fields.

Errors

  • 400 INVALID_SCHEDULE_CONFIG — proposed cron invalid.
  • 400 INVALID_ON_EXHAUSTED / 400 INVALID_RUNWAY_ALARM_DAYS / 400 INVALID_BATCH_SIZE.
  • 409 CONTENT_RUNWAY_NOT_READY — the project is mid-upgrade; retry in a few minutes.
  • 404 NOT_FOUND.
  • 502 SCHEDULE_UPDATE_FAILED — workflow runner refused the mutation; DB rolled back.
  • 500 UPDATE_FAILED.

Try it:

PATCH/admin/projects/%7B%7BprojectId%7D%7D/content/schedules/%7B%7BscheduleId%7D%7D
developer auth
curl -X PATCH 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/schedules/%7B%7BscheduleId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X PATCH "${BASE_URL}/admin/projects/${PROJECT_ID}/content/schedules/{scheduleId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{}'

DELETE /admin/projects/:projectId/content/schedules/:scheduleId

Delete the scheduled workflow handle first, then the row. Wrapped in a tx with FOR UPDATE so concurrent PATCHes can't recreate a scheduled workflow mid-delete.

Response 200

{ "data": { "deleted": true } }

Errors

  • 404 NOT_FOUND.
  • 502 SCHEDULE_DELETE_FAILED — workflow teardown failed; row still exists.
  • 500 DELETE_FAILED.

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/content/schedules/%7B%7BscheduleId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/content/schedules/%7B%7BscheduleId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/content/schedules/{scheduleId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"