Review and approve

Run the whole review inside your own product. Read what the AI drafted and priced, fix what is wrong, pick replacement offers and approve, without anyone opening Contents.team.

The review loop

  1. Wait for pricing. Listen for the items.priced webhook, or poll GET /api/v1/claims/{claimId} until analysis.status is complete. Test claims do not send items.priced yet, so poll in test mode.
  2. List what needs a person. GET .../items?needsReview=true returns the items waiting on a decision; add priced=false for the ones with no price yet.
  3. Fix what is wrong. PATCH /api/v1/items/{itemId} changes the name, quantity, price, room, condition, age or notes.
  4. Pick a replacement. GET .../items/{itemId}/replacements lists the offers the finder found; accepting one prices the item from the offer and approves it in the same step.
  5. Approve. POST /api/v1/claims/{claimId}/items/approve with up to 1000 item ids.
  6. 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).

statusMeaningWhat you can do
pendingDrafted and waiting on a personEdit, accept an offer, approve or reject
low_confidenceDrafted, but the AI was unsure; check it firstSame as pending
reviewingBeing priced right nowWait: edits and approval are a 409 item_locked until it finishes
approvedCounts toward totals and exportsEdit (it stays approved), unapprove, reject
rejectedKept on the claim but left out of totals and exportsApprove to bring it back
Request
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.

Request
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.

Request
curl https://contents.team/api/v1/items/itm_0DDfgQ4Z7/replacements \
  -H "Authorization: Bearer $CONTENTS_API_KEY"
Response
{
  "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.

Request
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)"
ErrorWhyWhat to do
400 quantity_confirmation_requiredThe finder could not tell how many units the listing coversSend quantity and quantityConfirmed: true
409 quantity_mismatchYour quantity disagrees with what the finder read off the listingCheck the listing; pick another offer or edit the item
409 replacement_staleA newer search replaced this offerList the offers again
409 item_already_approvedApproved items are never re-priced from an offerUnapprove first
409 item_lockedThe item is being pricedRetry 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.

Request
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"
    ]
  }'
Response
{
  "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.

Live keys need an organization with an active subscription; writes from a live key on an inactive one are a 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.

NextWebhooksEvent types, delivery and retries, and verifying the signature.