Review and approve
The review loop
- Wait for pricing. Listen for the
items.pricedwebhook, or pollGET /api/v1/claims/{claimId}untilanalysis.statusiscomplete. Test claims do not senditems.pricedyet, so poll in test mode. - List what needs a person.
GET .../items?needsReview=truereturns the items waiting on a decision; addpriced=falsefor the ones with no price yet. - Fix what is wrong.
PATCH /api/v1/items/{itemId}changes the name, quantity, price, room, condition, age or notes. - Pick a replacement.
GET .../items/{itemId}/replacementslists the offers the finder found; accepting one prices the item from the offer and approves it in the same step. - Approve.
POST /api/v1/claims/{claimId}/items/approvewith up to 1000 item ids. - Export.
POST /api/v1/claims/{claimId}/exports. Only approved items reach an export or the claim's totals.
Every one of these calls goes through the same code as the same action in the app: the same validation, price caps, locks, history and webhooks. An adjuster looking at the claim in Contents.team sees the change at once.
Scopes
Reading needs items:read; every change needs items:write. Write does not include read, so a key that reviews needs both, plus claims:read to poll the claim and exports:write to export. Create the key in Settings, or ask for the scopes in your OAuth app; see Authentication.
What needs attention
Each item carries status, plus two flags that answer the usual question directly: needsReview (waiting on a person) and priced (has a unit cost).
| status | Meaning | What you can do |
|---|---|---|
pending | Drafted and waiting on a person | Edit, accept an offer, approve or reject |
low_confidence | Drafted, but the AI was unsure; check it first | Same as pending |
reviewing | Being priced right now | Wait: edits and approval are a 409 item_locked until it finishes |
approved | Counts toward totals and exports | Edit (it stays approved), unapprove, reject |
rejected | Kept on the claim but left out of totals and exports | Approve to bring it back |
curl "https://contents.team/api/v1/claims/clm_0a6NnX3I9/items?needsReview=true&limit=100" \
-H "Authorization: Bearer $CONTENTS_API_KEY"An item with priced: false cannot be approved. Give it a price with unitCostCents, or accept one of its replacement offers.
Edit an item
Send only the fields to change. Money is integer cents; unitCostCents: null removes the price. Setting name replaces a retailer title chosen earlier. roomId must be a room on the same claim (GET .../claims/{claimId}/rooms), or null to take the item out of its room. The response is the item, revalued.
curl -X PATCH https://contents.team/api/v1/items/itm_0EXaAMAJl \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Samsung 65\" QLED 4K Smart TV (QN65Q80C)",
"unitCostCents": 119999,
"priceSource": "Best Buy",
"priceSourceUrl": "https://www.bestbuy.com/site/6536963.p"
}'PATCH and DELETE need no Idempotency-Key: repeating one leaves the item in the same state. Quantity times unit cost must stay within $99,999,999.99 (400 amount_out_of_range).
Replacement offers
The replacement finder searches retailers for each item. Its offers from the latest search come back best first, with proposed: true on the one it picked.
curl https://contents.team/api/v1/items/itm_0DDfgQ4Z7/replacements \
-H "Authorization: Bearer $CONTENTS_API_KEY"{
"object": "list",
"itemId": "itm_0DDfgQ4Z7",
"searchedAt": "2026-10-21T15:22:03.000Z",
"data": [
{
"id": "rpl_0W73r0WMy",
"object": "replacement",
"title": "KitchenAid Artisan 5 Qt Tilt-Head Stand Mixer",
"model": "KSM150PS",
"retailer": "Target",
"priceCents": 44999,
"url": "https://www.target.com/p/kitchenaid-artisan-5qt-stand-mixer/-/A-11033155",
"imageUrl": "https://target.scene7.com/is/image/Target/GUEST_ksm150ps",
"proposed": true,
"outsidePreferredRetailers": false,
"acceptedAt": null
}
]
}Accepting an offer prices the item from it and approves the item in one step. The price and quantity come from the stored offer, never from your request. Send keepItemTitle: true to keep the item's own name and take only the price and source. The body may be empty.
curl -X POST https://contents.team/api/v1/items/itm_0DDfgQ4Z7/replacements/rpl_0W73r0WMy/accept \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"| Error | Why | What to do |
|---|---|---|
400 quantity_confirmation_required | The finder could not tell how many units the listing covers | Send quantity and quantityConfirmed: true |
409 quantity_mismatch | Your quantity disagrees with what the finder read off the listing | Check the listing; pick another offer or edit the item |
409 replacement_stale | A newer search replaced this offer | List the offers again |
409 item_already_approved | Approved items are never re-priced from an offer | Unapprove first |
409 item_locked | The item is being priced | Retry once it finishes |
Approve
Approve up to 1000 items of one claim per call. The response sorts every id into one list, so nothing is silently dropped: approved, alreadyApproved, locked (being priced; retry later), skipped with a reason (unpriced items cannot be approved) and notFound.
curl -X POST https://contents.team/api/v1/claims/clm_0a6NnX3I9/items/approve \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"itemIds": [
"itm_0EXaAMAJl",
"itm_0KgmKY3ZZ",
"itm_0DDfgQ4Z7",
"itm_0gLuRknbc"
]
}'{
"object": "item_approval",
"claimId": "clm_0a6NnX3I9",
"approved": [
"itm_0KgmKY3ZZ",
"itm_0DDfgQ4Z7"
],
"alreadyApproved": [
"itm_0EXaAMAJl"
],
"locked": [],
"skipped": [
{
"itemId": "itm_0gLuRknbc",
"reason": "unpriced"
}
],
"notFound": []
}POST .../items/unapprove moves approved items back to pending, and POST .../items/reject rejects up to 100 items. Both take the same itemIds body and answer with the same kind of lists.
Add or remove items
POST /api/v1/claims/{claimId}/items adds a line by hand, as Add item does in the app: the item is manual and approved at once. DELETE /api/v1/items/{itemId} removes one from the claim, its totals and its exports; its history is kept.
Webhooks
Approving, by bulk approval or by accepting an offer, sends inventory.approved with the newly approved item ids, the same event an approval in the app sends. Items added by hand do not send it. Events from a test claim go to test endpoints only. See Webhooks.
Billing
An approval through the API is the same status change as an approval in the app, and the same daily job, the only place usage is metered, counts it. Approving through the API costs nothing extra and never counts an item twice. Items on a test claim are never billed.
402 billing_inactive. Test keys always work.From an assistant
The MCP server has the same loop as tools: get_inventory with needsReview, update_item, get_replacements, accept_replacement, add_item, reject_items, delete_item, approve_items and unapprove_items. See MCP server.