API reference
Generated from the OpenAPI document the API serves, so it is always the deployed truth. Download it as JSON or YAML for code generators, or start with the quickstart.
Every request authenticates with Authorization: Bearer and an org API key or OAuth token. POSTs need an Idempotency-Key. Errors share one envelope; see requests and errors.
Claims
List claims
get/api/v1/claims
Newest first. Page with cursor = the previous page's nextCursor. To sync, note the time before you start, page through with updatedSince = your last sync time, then use the noted time next run.
Scope: claims:read
Parameters
limitinteger · queryPage size, 1-100.
cursorstring · querynextCursorfrom the previous page.externalRefstring · queryOnly claims with this externalRef.
claimNumberstring · queryOnly claims with this whole claim number (not part of one; case does not matter): the number people usually call a claim by.
updatedSincestring (date-time) · queryOnly claims whose record changed at or after this instant.
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequiredclm_ id.
data[].object"claim"requireddata[].mode"live" | "test"requiredtest = created with a ctk_test_ key: only test keys see it, and it is never billed.
data[].externalRefstring | nullrequiredYour own id for this claim.
data[].claimNumberstring | nullrequireddata[].insuredNamestring | nullrequireddata[].lossAddressstring | nullrequireddata[].lossType"Fire" | "Water" | "Wind" | "Other" | nullrequireddata[].dateOfLossstring | nullrequiredYYYY-MM-DD.
data[].status"open" | "in_progress" | "review" | "closed"requireddata[].insuranceCompanystring | nullrequireddata[].policyNumberstring | nullrequireddata[].valuationMode"rcv" | "acv" | "acv_only"requireddata[].salesTaxBpinteger | nullrequiredSales tax in basis points (825 = 8.25%).
data[].itemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
data[].approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included (get_claim_status has the pre-tax approvedSubtotalCents).
data[].createdAtstring (date-time)requireddata[].updatedAtstring (date-time)requiredLast change to the claim record itself (not its items).
hasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/claims' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Create a claim
post/api/v1/claims
Creates a claim in the key's organization. Every field is optional; set externalRef to your own id so you can find the claim again with GET /api/v1/claims?externalRef=. A test key (ctk_test_) creates a test claim: only test keys see it and it is never billed. Live keys need an organization with an active subscription (402 billing_inactive otherwise).
Scope: claims:write
Parameters
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
externalRefstring | nullYour own id for this claim; filter on it with GET /api/v1/claims?externalRef=. Not unique.
claimNumberstring | nullThe carrier claim number, shown as the claim's label in the app.
insuredNamestring | nullThe policyholder (your customer).
lossAddressstring | nulllossType"Fire" | "Water" | "Wind" | "Other" | nulldateOfLossstring (date) | nullYYYY-MM-DD.
insuranceCompanystring | nullpolicyNumberstring | nullsalesTaxBpinteger | nullSales tax in basis points (825 = 8.25%).
›Response 201Success
idstringrequiredclm_ id.
object"claim"requiredmode"live" | "test"requiredtest = created with a ctk_test_ key: only test keys see it, and it is never billed.
externalRefstring | nullrequiredYour own id for this claim.
claimNumberstring | nullrequiredinsuredNamestring | nullrequiredlossAddressstring | nullrequiredlossType"Fire" | "Water" | "Wind" | "Other" | nullrequireddateOfLossstring | nullrequiredYYYY-MM-DD.
status"open" | "in_progress" | "review" | "closed"requiredinsuranceCompanystring | nullrequiredpolicyNumberstring | nullrequiredvaluationMode"rcv" | "acv" | "acv_only"requiredsalesTaxBpinteger | nullrequiredSales tax in basis points (825 = 8.25%).
itemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included (get_claim_status has the pre-tax approvedSubtotalCents).
createdAtstring (date-time)requiredupdatedAtstring (date-time)requiredLast change to the claim record itself (not its items).
curl -X POST 'https://contents.team/api/v1/claims' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"externalRef":"string","claimNumber":"string","insuredName":"string"}'Get a claim
get/api/v1/claims/{claimId}
The claim with its item counts, approved totals (valued with the claim's tax, depreciation and payment caps) and photo-analysis progress. Poll it to know when analysis has finished. An id from another organization is answered exactly like an unknown id: 404.
Scope: claims:read
Parameters
claimIdstring · pathrequiredclm_ id.
›Response 200Success
idstringrequiredclm_ id.
object"claim"requiredmode"live" | "test"requiredtest = created with a ctk_test_ key: only test keys see it, and it is never billed.
externalRefstring | nullrequiredYour own id for this claim.
claimNumberstring | nullrequiredinsuredNamestring | nullrequiredlossAddressstring | nullrequiredlossType"Fire" | "Water" | "Wind" | "Other" | nullrequireddateOfLossstring | nullrequiredYYYY-MM-DD.
status"open" | "in_progress" | "review" | "closed"requiredinsuranceCompanystring | nullrequiredpolicyNumberstring | nullrequiredvaluationMode"rcv" | "acv" | "acv_only"requiredsalesTaxBpinteger | nullrequiredSales tax in basis points (825 = 8.25%).
itemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included (get_claim_status has the pre-tax approvedSubtotalCents).
createdAtstring (date-time)requiredupdatedAtstring (date-time)requiredLast change to the claim record itself (not its items).
countsobjectrequiredcounts.itemsintegerrequiredInventory lines, excluding auto-detected duplicates.
counts.itemsAwaitingReviewintegerrequiredpending and low_confidence items.
counts.itemsApprovedintegerrequiredcounts.itemsRejectedintegerrequiredcounts.roomsintegerrequiredcounts.photosintegerrequiredtotalsobjectrequiredApproved items only, valued with the claim's tax, depreciation and payment caps.
totals.approvedSubtotalCentsintegerrequiredtotals.approvedTaxCentsintegerrequiredtotals.approvedRcvCentsintegerrequiredtotals.approvedAcvCentsintegerrequiredtotals.approvedPaymentCentsintegerrequiredWhat the approved lines pay after payment caps.
analysisobjectrequiredanalysis.status"no_photos" | "in_progress" | "awaiting_start" | "complete"requiredin_progress while any inventory photo is queued or analyzing; awaiting_start when the only unanalyzed photos are staged (held until someone starts analysis in the app); complete otherwise.
analysis.photosTotalintegerrequiredInventory photos (reference photos are never analyzed).
analysis.photosQueuedintegerrequiredUploaded and waiting for analysis.
analysis.photosStagedintegerrequiredanalysis.photosAnalyzingintegerrequiredanalysis.photosCompletedintegerrequiredanalysis.photosFailedintegerrequiredanalysis.photosAttachedintegerrequiredAdded to an existing item as extra evidence (attach_item_photos, or the app's add photo to item): stored, never analyzed.
curl -X GET 'https://contents.team/api/v1/claims/{claimId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Update a claim
patch/api/v1/claims/{claimId}
Change any of the listed fields of the claim record; leave out the ones to keep, send null to clear one. At least one field. Status, mode, valuation and sales tax cannot be changed here (an unknown field is a 400). Open app tabs refresh. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: claims:write
Parameters
claimIdstring · pathrequiredclm_ id.
Request body
externalRefstring | nullYour own id for this claim; filter on it with GET /api/v1/claims?externalRef=. Not unique.
claimNumberstring | nullThe carrier claim number, shown as the claim's label in the app.
insuredNamestring | nullThe policyholder (your customer).
lossAddressstring | nulllossType"Fire" | "Water" | "Wind" | "Other" | nulldateOfLossstring (date) | nullYYYY-MM-DD.
insuranceCompanystring | nullpolicyNumberstring | null
›Response 200Success
idstringrequiredclm_ id.
object"claim"requiredmode"live" | "test"requiredtest = created with a ctk_test_ key: only test keys see it, and it is never billed.
externalRefstring | nullrequiredYour own id for this claim.
claimNumberstring | nullrequiredinsuredNamestring | nullrequiredlossAddressstring | nullrequiredlossType"Fire" | "Water" | "Wind" | "Other" | nullrequireddateOfLossstring | nullrequiredYYYY-MM-DD.
status"open" | "in_progress" | "review" | "closed"requiredinsuranceCompanystring | nullrequiredpolicyNumberstring | nullrequiredvaluationMode"rcv" | "acv" | "acv_only"requiredsalesTaxBpinteger | nullrequiredSales tax in basis points (825 = 8.25%).
itemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included (get_claim_status has the pre-tax approvedSubtotalCents).
createdAtstring (date-time)requiredupdatedAtstring (date-time)requiredLast change to the claim record itself (not its items).
curl -X PATCH 'https://contents.team/api/v1/claims/{claimId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalRef":"string","claimNumber":"string","insuredName":"string"}'Photos
List a claim's photos
get/api/v1/claims/{claimId}/photos
Photos oldest first, with analysis status and signed download URLs that expire after 15 minutes. Page with cursor = the previous page's nextCursor. An id from another organization is answered exactly like an unknown id: 404.
Scope: photos:read
Parameters
claimIdstring · pathrequiredclm_ id.
limitinteger · queryPage size, 1-100.
cursorstring · querynextCursorfrom the previous page.roomIdstring · queryOnly photos filed in this room (rom_ id).
updatedSincestring (date-time) · queryOnly photos changed (including analysis status) at or after this instant.
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequiredpho_ id.
data[].object"photo"requireddata[].claimIdstringrequiredclm_ id.
data[].roomIdstring | nullrequiredrom_ id, or null when the photo is not filed in a room.
data[].filenamestringrequireddata[].contentTypestringrequireddata[].sizeBytesinteger | nullrequireddata[].kind"inventory" | "reference"requiredreference: a room overview shot, never analyzed for items.
data[].analysisStatus"queued" | "staged" | "analyzing" | "completed" | "failed" | "not_analyzed"requiredqueued: uploaded, waiting for analysis; staged: held until someone starts analysis in the app; analyzing; completed: items identified (possibly none); failed: analysis failed; not_analyzed: a reference photo, or one added to an existing item as extra evidence (never analyzed).
data[].stackIdstring | nullrequiredpho_ id of the stack lead when several photos show one item; null otherwise.
data[].urlsobjectrequiredSigned GET URLs. They expire at urlsExpireAt: fetch the photo again for fresh ones, never store them.
data[].urls.originalstringrequiredThe uploaded file (may be HEIC).
data[].urls.compressedstringrequiredA WebP copy for display. Generated shortly after upload: until then this URL returns 404, so fall back to original.
data[].urls.thumbnailstringrequireddata[].urlsExpireAtstring (date-time)requireddata[].createdAtstring (date-time)requireddata[].updatedAtstring (date-time)requiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/claims/{claimId}/photos' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Add photos by URL
post/api/v1/claims/{claimId}/photos
Up to 10 public https image URLs per call. We fetch each once (JPEG, PNG, WebP or HEIC, at most 20 MB; private and internal addresses are refused) and send the photos into the same AI pipeline as photos uploaded in the app. URLs that fail are listed in failed; if none can be fetched the call is a 400. With a test key, photos are analyzed by the fixture pipeline: canned, deterministic items appear at once and nothing is billed. Send X-Contents-Test-Mode: live-ai to run the real pipeline instead, capped at 50 photos per organization per UTC day (429 test_live_ai_cap_reached beyond that).
Scope: photos:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
photosobject[]requiredphotos[].urlstring (uri)requiredPublic https URL of a JPEG, PNG, WebP or HEIC image, at most 20 MB. Fetched once, server-side.
photos[].roomNamestringRoom to file the photo under; created on the claim if it does not exist yet.
photos[].externalIdstringYour own id for the photo, echoed back; also picks the test-mode fixture set.
›Response 201Success
object"photo_batch"requiredclaimIdstringrequiredpipeline"ai" | "test_fixtures" | "test_live_ai"requiredai = the real pipeline (live keys); test_fixtures = canned items at once; test_live_ai = real pipeline, test claim.
photosobject[]requiredphotos[].idstringrequiredpho_ id.
photos[].object"photo"requiredphotos[].externalIdstring | nullrequiredphotos[].filenamestringrequiredphotos[].roomIdstring | nullrequiredrom_ id, or null when unfiled.
photos[].status"uploading" | "processing" | "analyzed" | "analysis_failed" | "ready" | "failed"requiredprocessing = queued for or in AI analysis; analyzed = items extracted; ready = stored, not analyzed; failed = the upload never arrived or did not match.
failedobject[]requiredfailed[].indexinteger | nullrequiredPosition in the request, when the failure is about one entry.
failed[].photoIdstring | nullrequiredfailed[].externalIdstring | nullrequiredfailed[].codestringrequiredfailed[].messagestringrequired
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/photos' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"photos":[{"url":"https://example.com/photo.jpg"}]}'Get a photo
get/api/v1/photos/{photoId}
One photo with its analysis status and fresh signed URLs. An id from another organization is answered exactly like an unknown id: 404.
Scope: photos:read
Parameters
photoIdstring · pathrequiredpho_ id.
›Response 200Success
idstringrequiredpho_ id.
object"photo"requiredclaimIdstringrequiredclm_ id.
roomIdstring | nullrequiredrom_ id, or null when the photo is not filed in a room.
filenamestringrequiredcontentTypestringrequiredsizeBytesinteger | nullrequiredkind"inventory" | "reference"requiredreference: a room overview shot, never analyzed for items.
analysisStatus"queued" | "staged" | "analyzing" | "completed" | "failed" | "not_analyzed"requiredqueued: uploaded, waiting for analysis; staged: held until someone starts analysis in the app; analyzing; completed: items identified (possibly none); failed: analysis failed; not_analyzed: a reference photo, or one added to an existing item as extra evidence (never analyzed).
stackIdstring | nullrequiredpho_ id of the stack lead when several photos show one item; null otherwise.
urlsobjectrequiredSigned GET URLs. They expire at urlsExpireAt: fetch the photo again for fresh ones, never store them.
urls.originalstringrequiredThe uploaded file (may be HEIC).
urls.compressedstringrequiredA WebP copy for display. Generated shortly after upload: until then this URL returns 404, so fall back to original.
urls.thumbnailstringrequiredurlsExpireAtstring (date-time)requiredcreatedAtstring (date-time)requiredupdatedAtstring (date-time)required
curl -X GET 'https://contents.team/api/v1/photos/{photoId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Start a direct photo upload
post/api/v1/claims/{claimId}/photo-uploads
Register up to 24 files and get one presigned PUT URL per file (valid 5 minutes). PUT each file's exact bytes with the returned headers, then call .../photo-uploads/complete. Nothing is analyzed until then.
Scope: photos:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
filesobject[]requiredfiles[].filenamestringrequiredfiles[].contentType"image/jpeg" | "image/png" | "image/webp" | "image/heic" | "image/heif"requiredfiles[].sizeBytesintegerrequiredExact byte size you will PUT.
files[].roomNamestringRoom to file the photo under; created on the claim if it does not exist yet.
files[].externalIdstringYour own id for the photo, echoed back; also picks the test-mode fixture set.
›Response 201Success
idstringrequiredupl_ id; pass it to the complete call.
object"photo_upload"requiredclaimIdstringrequireduploadsobject[]requireduploads[].photoIdstringrequireduploads[].externalIdstring | nullrequireduploads[].filenamestringrequireduploads[].method"PUT"requireduploads[].urlstringrequiredPresigned storage URL. PUT the exact bytes with the headers below.
uploads[].headersobjectrequireduploads[].expiresAtstring (date-time)required
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/photo-uploads' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"files":[{"filename":"string","contentType":"image/jpeg","sizeBytes":1}]}'Finish a direct photo upload
post/api/v1/claims/{claimId}/photo-uploads/complete
Checks each uploaded file arrived with the declared size and type, then sends the photos into the AI pipeline. Files that did not arrive are listed in failed. With a test key, photos are analyzed by the fixture pipeline: canned, deterministic items appear at once and nothing is billed. Send X-Contents-Test-Mode: live-ai to run the real pipeline instead, capped at 50 photos per organization per UTC day (429 test_live_ai_cap_reached beyond that).
Scope: photos:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
uploadIdstringrequiredupl_ id from the create call.
photoIdsstring[]requiredpho_ ids you finished uploading.
›Response 200Success
object"photo_batch"requiredclaimIdstringrequiredpipeline"ai" | "test_fixtures" | "test_live_ai"requiredai = the real pipeline (live keys); test_fixtures = canned items at once; test_live_ai = real pipeline, test claim.
photosobject[]requiredphotos[].idstringrequiredpho_ id.
photos[].object"photo"requiredphotos[].externalIdstring | nullrequiredphotos[].filenamestringrequiredphotos[].roomIdstring | nullrequiredrom_ id, or null when unfiled.
photos[].status"uploading" | "processing" | "analyzed" | "analysis_failed" | "ready" | "failed"requiredprocessing = queued for or in AI analysis; analyzed = items extracted; ready = stored, not analyzed; failed = the upload never arrived or did not match.
failedobject[]requiredfailed[].indexinteger | nullrequiredPosition in the request, when the failure is about one entry.
failed[].photoIdstring | nullrequiredfailed[].externalIdstring | nullrequiredfailed[].codestringrequiredfailed[].messagestringrequired
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/photo-uploads/complete' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"uploadId":"string","photoIds":["string"]}'Rooms
List a claim's rooms
get/api/v1/claims/{claimId}/rooms
Every room in display order, in one page. Floors hold rooms, rooms hold subrooms (parentId). An id from another organization is answered exactly like an unknown id: 404.
Scope: claims:read
Parameters
claimIdstring · pathrequiredclm_ id.
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequiredrom_ id.
data[].object"room"requireddata[].claimIdstringrequiredclm_ id of the claim the room belongs to.
data[].namestringrequireddata[].type"floor" | "room" | "subroom"requireddata[].parentIdstring | nullrequiredrom_ id of the containing floor or room; null at the top level.
data[].displayOrderintegerrequiredPosition among its siblings, as the app orders them.
data[].itemCountintegerrequiredInventory lines filed in this room (not its subrooms), excluding auto-detected duplicates.
data[].createdAtstring (date-time)requireddata[].updatedAtstring (date-time)requiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/claims/{claimId}/rooms' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Items
Get an item
get/api/v1/items/{itemId}
One inventory item, valued within its claim. An id from another organization is answered exactly like an unknown id: 404.
Scope: items:read
Parameters
itemIdstring · pathrequireditm_ id.
›Response 200Success
idstringrequireditm_ id.
object"item"requiredclaimIdstringrequiredclm_ id.
roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
roomNamestring | nullrequirednamestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
retailerDescriptionstring | nullrequiredmodelstring | nullrequiredcategorystring | nullrequiredquantityintegerrequiredunitCostCentsinteger | nullrequiredNull until the item is priced.
pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
condition"New" | "Above Average" | "Average" | "Below Average" | nullrequiredestimatedAgeYearsnumber | nullrequiredsource"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
priceSourcestring | nullrequiredWhere the price came from (retailer name).
priceSourceUrlstring | nullrequirednotesstring | nullrequiredphotoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
valuationobjectrequiredvaluation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
valuation.depreciationCentsintegerrequiredvaluation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
curl -X GET 'https://contents.team/api/v1/items/{itemId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Update an item
patch/api/v1/items/{itemId}
Change any of the listed fields; leave out the ones to keep. category takes a name from GET /api/v1/claims/{claimId}/categories (or, on a claim whose taxonomy is xactimate, its 3-letter code); an unknown one is a 400 parameter_invalid. The same validation and price caps as the app apply, and the change is recorded in the item's history like an edit in the app. Editing an approved item keeps it approved. An item being priced (status reviewing) is a 409 item_locked: retry once it finishes. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
itemIdstring · pathrequireditm_ id.
Request body
namestringThe line's text as the claim shows it. Setting it replaces a retailer title chosen earlier.
modelstring | nullMake and model, if known.
quantityinteger1-100000.
unitCostCentsinteger | nullReplacement price for one, in integer cents; null removes the price (the item is then unpriced and cannot be approved).
priceSourcestring | nullWhere the price came from (a retailer name).
priceSourceUrlstring (uri) | nullThe listing the price came from. Google search links are dropped, as in the app.
roomIdstring | nullrom_ id of a room on the same claim (from list_rooms, or GET /api/v1/claims/{claimId}/rooms); null takes the item out of its room.
categorystring | nullCategory name from list_categories (GET /api/v1/claims/{claimId}/categories), or its 3-letter code on claims that use Contents.team categories (taxonomy xactimate); null clears it. Drives depreciation.
condition"New" | "Above Average" | "Average" | "Below Average" | nullestimatedAgeYearsnumber | nullAge in years, to a tenth (2.5). Drives depreciation.
notesstring | null
›Response 200Success
idstringrequireditm_ id.
object"item"requiredclaimIdstringrequiredclm_ id.
roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
roomNamestring | nullrequirednamestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
retailerDescriptionstring | nullrequiredmodelstring | nullrequiredcategorystring | nullrequiredquantityintegerrequiredunitCostCentsinteger | nullrequiredNull until the item is priced.
pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
condition"New" | "Above Average" | "Average" | "Below Average" | nullrequiredestimatedAgeYearsnumber | nullrequiredsource"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
priceSourcestring | nullrequiredWhere the price came from (retailer name).
priceSourceUrlstring | nullrequirednotesstring | nullrequiredphotoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
valuationobjectrequiredvaluation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
valuation.depreciationCentsintegerrequiredvaluation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
curl -X PATCH 'https://contents.team/api/v1/items/{itemId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"string","model":"string","quantity":1}'Delete an item
DEL/api/v1/items/{itemId}
Removes the item from the claim, its totals and its exports, as deleting it in the app does (the line's history is kept). 409 item_locked while it is being priced. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
itemIdstring · pathrequireditm_ id.
Response 204
No content
curl -X DELETE 'https://contents.team/api/v1/items/{itemId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"List the categories items on a claim can take
get/api/v1/claims/{claimId}/categories
Every category of the claim's taxonomy, by name, in one page, with its default depreciation. Set an item's category with PATCH /api/v1/items/{itemId} category: the name, or on a claim whose taxonomy is xactimate the name or the 3-letter code. An id from another organization is answered exactly like an unknown id: 404.
Scope: items:read
Parameters
claimIdstring · pathrequiredclm_ id.
›Response 200Success
object"list"requireddataobject[]requireddata[].object"category"requireddata[].namestringrequiredPass it as
categorywhen updating an item on this claim.data[].codestring | nullrequiredThe 3-letter code on claims whose taxonomy is xactimate (also accepted as
category); null otherwise.data[].depreciationType"annual" | "1st-year" | "flat" | nullrequiredHow the category depreciates by default: annual (per year of age), 1st-year (once), flat (age ignored).
data[].depreciationBpinteger | nullrequiredDefault depreciation rate in basis points (1000 = 10%).
data[].maxDepreciationBpinteger | nullrequiredDefault depreciation cap in basis points; null = no cap.
data[].usefulLifeYearsinteger | nullrequiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.taxonomy"jmidg" | "xactimate"requiredThe claim's category system. The claim's own depreciation settings can override the defaults listed.
curl -X GET 'https://contents.team/api/v1/claims/{claimId}/categories' \
-H "Authorization: Bearer $CONTENTS_API_KEY"List a claim's items
get/api/v1/claims/{claimId}/items
The inventory, oldest first. Each item carries its RCV/ACV valuation in cents and its depreciation in basis points. Page with cursor = the previous page's nextCursor. An id from another organization is answered exactly like an unknown id: 404.
Scope: items:read
Parameters
claimIdstring · pathrequiredclm_ id.
limitinteger · queryPage size, 1-100.
cursorstring · querynextCursorfrom the previous page.status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected" · queryOnly items with this status.
needsReview"true" | "false" · querytrue: only items waiting on a person (pending or low_confidence); false: only the others.
priced"true" | "false" · querytrue: only items with a unit cost; false: only unpriced items.
roomIdstring · queryOnly items filed in this room (rom_ id).
updatedSincestring (date-time) · queryOnly items changed at or after this instant.
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequireditm_ id.
data[].object"item"requireddata[].claimIdstringrequiredclm_ id.
data[].roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
data[].roomNamestring | nullrequireddata[].namestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
data[].descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
data[].retailerDescriptionstring | nullrequireddata[].modelstring | nullrequireddata[].categorystring | nullrequireddata[].quantityintegerrequireddata[].unitCostCentsinteger | nullrequiredNull until the item is priced.
data[].pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
data[].status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
data[].needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
data[].condition"New" | "Above Average" | "Average" | "Below Average" | nullrequireddata[].estimatedAgeYearsnumber | nullrequireddata[].source"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
data[].priceSourcestring | nullrequiredWhere the price came from (retailer name).
data[].priceSourceUrlstring | nullrequireddata[].notesstring | nullrequireddata[].photoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
data[].valuationobjectrequireddata[].valuation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
data[].valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
data[].valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
data[].valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
data[].valuation.depreciationCentsintegerrequireddata[].valuation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
data[].valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
data[].createdAtstring (date-time)requireddata[].updatedAtstring (date-time)requiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/claims/{claimId}/items' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Add an item
post/api/v1/claims/{claimId}/items
Adds a line by hand, as the app's Add item does: source manual, and approved as soon as it is added. It counts toward totals and exports at once; an unpriced one counts at $0 until you set unitCostCents. Adding an item does not send inventory.approved. Approval through the API is the same status change as in the app: the daily settle job, the one place usage is metered, treats it exactly like an approval made in the app. Items on a test claim are never billed. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
namestringrequiredThe line's text as the claim shows it. Setting it replaces a retailer title chosen earlier.
modelstring | nullMake and model, if known.
quantityinteger1-100000, default 1.
unitCostCentsinteger | nullReplacement price for one, in integer cents. Omit to add the item unpriced.
roomIdstring | nullrom_ id of a room on the same claim (from list_rooms, or GET /api/v1/claims/{claimId}/rooms); null takes the item out of its room.
condition"New" | "Above Average" | "Average" | "Below Average" | nullestimatedAgeYearsnumber | nullAge in years, to a tenth (2.5). Drives depreciation.
notesstring | null
›Response 201Success
idstringrequireditm_ id.
object"item"requiredclaimIdstringrequiredclm_ id.
roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
roomNamestring | nullrequirednamestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
retailerDescriptionstring | nullrequiredmodelstring | nullrequiredcategorystring | nullrequiredquantityintegerrequiredunitCostCentsinteger | nullrequiredNull until the item is priced.
pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
condition"New" | "Above Average" | "Average" | "Below Average" | nullrequiredestimatedAgeYearsnumber | nullrequiredsource"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
priceSourcestring | nullrequiredWhere the price came from (retailer name).
priceSourceUrlstring | nullrequirednotesstring | nullrequiredphotoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
valuationobjectrequiredvaluation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
valuation.depreciationCentsintegerrequiredvaluation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/items' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"string"}'List an item's replacement offers
get/api/v1/items/{itemId}/replacements
The offers the replacement finder found for the item in its most recent search, best first, as the app shows them. proposed marks the finder's pick. An item never searched has an empty list. An id from another organization is answered exactly like an unknown id: 404.
Scope: items:read
Parameters
itemIdstring · pathrequireditm_ id.
›Response 200Success
object"list"requireditemIdstringrequiredsearchedAtstring (date-time) | nullrequiredWhen the finder last searched for this item; null if it never has.
searchingbooleanrequiredTrue while a search for this item is running (the item is reviewing): the offers are still the previous search's. Call again in a little while.
dataobject[]requiredThe offers from the most recent search, best first.
data[].idstringrequiredrpl_ id; pass it to .../replacements/{replacementId}/accept.
data[].object"replacement"requireddata[].titlestringrequiredThe listing's product title.
data[].modelstring | nullrequireddata[].retailerstring | nullrequireddata[].priceCentsinteger | nullrequiredThe listing's price for one, in integer cents; null when the listing had no price.
data[].urlstring | nullrequiredThe retailer's product page, when one was found.
data[].imageUrlstring | nullrequireddata[].proposedbooleanrequiredTrue for the offer the finder picked for this item.
data[].outsidePreferredRetailersbooleanrequiredTrue when the retailer is not on the organization's preferred list.
data[].acceptedAtstring (date-time) | nullrequiredWhen this offer was applied to the item, if it was.
curl -X GET 'https://contents.team/api/v1/items/{itemId}/replacements' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Approve items
post/api/v1/claims/{claimId}/items/approve
Approves up to 1000 items of the claim at once, exactly as bulk approval in the app does. Each id lands in one list: approved, alreadyApproved, locked (still being priced; retry later), skipped (with a reason: unpriced items cannot be approved) or notFound. Only approved items count toward claim totals and exports. Newly approved items send one inventory.approved webhook. Approval through the API is the same status change as in the app: the daily settle job, the one place usage is metered, treats it exactly like an approval made in the app. Items on a test claim are never billed. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
itemIdsstring[]requireditm_ ids of items on this claim, 1-1000.
›Response 200Success
object"item_approval"requiredclaimIdstringrequiredapprovedstring[]requireditm_ ids approved by this request.
alreadyApprovedstring[]requireditm_ ids that were approved already; nothing changed.
lockedstring[]requireditm_ ids still being priced (status reviewing). Retry them once pricing finishes.
skippedobject[]requiredskipped[].itemIdstringrequiredskipped[].reason"unpriced" | "not_approvable"requiredunpriced: the item has no unit cost; set unitCostCents or accept a replacement, then approve again. not_approvable: the item cannot be approved in its current state.
notFoundstring[]requiredIds that are not items on this claim (or not visible to this key).
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/items/approve' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"itemIds":["string"]}'Reject items
post/api/v1/claims/{claimId}/items/reject
Rejects up to 100 items, as rejecting in the app does: they stay on the claim but are left out of its totals and exports. Approve one later to bring it back. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
itemIdsstring[]requireditm_ ids of items on this claim, 1-100.
›Response 200Success
object"item_rejection"requiredclaimIdstringrequiredrejectedstring[]requireditm_ ids rejected by this request.
alreadyRejectedstring[]requiredlockedstring[]requireditm_ ids still being priced (status reviewing).
notFoundstring[]required
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/items/reject' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"itemIds":["string"]}'Unapprove items
post/api/v1/claims/{claimId}/items/unapprove
Moves approved items back to pending (awaiting review), as undoing an approval in the app does. Items that are not approved are listed in notApproved and left alone. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
itemIdsstring[]requireditm_ ids of items on this claim, 1-1000.
›Response 200Success
object"item_unapproval"requiredclaimIdstringrequiredunapprovedstring[]requireditm_ ids moved from approved back to pending.
notApprovedstring[]requireditm_ ids that were not approved; nothing changed.
notFoundstring[]required
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/items/unapprove' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"itemIds":["string"]}'Price an item from a product link
post/api/v1/items/{itemId}/price-from-link
Reads a retailer's product page, as pasting a link in the app does, and sets the item's product name, model, store, link and, when the page gives one, unit price. The link must be an https product page on a public host (400 parameter_invalid on url otherwise; Google links are never a price source). A page with no price changes nothing on an item that already has a price (priced false). When a live key's page cannot be read, the price may come from the same store's shopping listing for the same product (priceMethod search): check it against the page. Test keys never make that search. With approve: true the item is then approved, only when the page itself priced it; a search price is never approved here (approveSkipped price_via_search: check it, then approve the item). approveSkipped says why not. 20 lookups a minute per credential. 409 item_locked while the item is being priced. Approval through the API is the same status change as in the app: the daily settle job, the one place usage is metered, treats it exactly like an approval made in the app. Items on a test claim are never billed. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
itemIdstring · pathrequireditm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
urlstringrequiredThe product page on the retailer's own site (https; an http link is read as https). Google search and shopping links are not product pages.
approvebooleantrue: approve the item too, but only when the page itself gave the price (otherwise approveSkipped says why it was not; a price found via search is never approved here).
›Response 200Success
object"item_price_from_link"requireditemobjectrequireditem.idstringrequireditm_ id.
item.object"item"requireditem.claimIdstringrequiredclm_ id.
item.roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
item.roomNamestring | nullrequireditem.namestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
item.descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
item.retailerDescriptionstring | nullrequireditem.modelstring | nullrequireditem.categorystring | nullrequireditem.quantityintegerrequireditem.unitCostCentsinteger | nullrequiredNull until the item is priced.
item.pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
item.status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
item.needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
item.condition"New" | "Above Average" | "Average" | "Below Average" | nullrequireditem.estimatedAgeYearsnumber | nullrequireditem.source"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
item.priceSourcestring | nullrequiredWhere the price came from (retailer name).
item.priceSourceUrlstring | nullrequireditem.notesstring | nullrequireditem.photoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
item.valuationobjectrequireditem.valuation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
item.valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
item.valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
item.valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
item.valuation.depreciationCentsintegerrequireditem.valuation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
item.valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
item.createdAtstring (date-time)requireditem.updatedAtstring (date-time)requiredpricedbooleanrequiredTrue when the link gave a price and it is now the item's unit cost. False: no price was found; an item that had a price is unchanged, and an unpriced item gets the page's store and link.
priceMethod"page" | "search" | nullrequiredpage: read from the product page. search: the page could not be read, so the price is the same store's listing for the same product in shopping search; check it against the page. null when no price was found.
approvedbooleanrequiredTrue when the item is approved after this call.
approveSkipped"unpriced" | "not_approvable" | "price_via_search"Only when approve was sent and the item was not approved: unpriced (the link gave no price), price_via_search (the price came from shopping search: check it against the page, then approve with approve_items), not_approvable (the item cannot be approved in its current state).
curl -X POST 'https://contents.team/api/v1/items/{itemId}/price-from-link' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"string"}'Accept a replacement offer
post/api/v1/items/{itemId}/replacements/{replacementId}/accept
Prices the item from one of its offers and approves it, in one step, as accepting in the app does. The price and the quantity come from the stored offer, never the request; when the finder could not tell how many units a listing covers, the call is a 400 quantity_confirmation_required and you confirm with quantity and quantityConfirmed. Only an offer from the latest search can be accepted (409 replacement_stale otherwise), and never on an approved item (409 item_already_approved). Sends inventory.approved. The body may be empty. Approval through the API is the same status change as in the app: the daily settle job, the one place usage is metered, treats it exactly like an approval made in the app. Items on a test claim are never billed. Live keys need an organization with an active subscription (402 billing_inactive otherwise). An id from another organization is answered exactly like an unknown id: 404.
Scope: items:write
Parameters
itemIdstring · pathrequireditm_ id.
replacementIdstring · pathrequiredrpl_ id from GET /api/v1/items/{itemId}/replacements.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
quantityintegerOnly when the finder could not tell how many units the listing covers: the quantity you are confirming. Send it with quantityConfirmed: true.
quantityConfirmedtruekeepItemTitletrueAdopt the listing's price and source but keep the item's own title.
›Response 200Success
idstringrequireditm_ id.
object"item"requiredclaimIdstringrequiredclm_ id.
roomIdstring | nullrequiredrom_ id, or null when the item is not filed in a room.
roomNamestring | nullrequirednamestringrequiredThe line as the app shows it: the retailer title when chosen, else the description.
descriptionstringrequiredWhat the item is, as identified or typed; name differs when a retailer title was chosen.
retailerDescriptionstring | nullrequiredmodelstring | nullrequiredcategorystring | nullrequiredquantityintegerrequiredunitCostCentsinteger | nullrequiredNull until the item is priced.
pricedbooleanrequiredTrue once the item has a unit cost. An unpriced item cannot be approved: set unitCostCents or accept a replacement first.
status"pending" | "low_confidence" | "reviewing" | "approved" | "rejected"requiredpending and low_confidence await review; reviewing is being priced (the item is locked: edits and approval are refused until it finishes); approved items count toward totals and exports; rejected items are left out of both.
needsReviewbooleanrequiredTrue while the item waits on a person: status pending or low_confidence.
condition"New" | "Above Average" | "Average" | "Below Average" | nullrequiredestimatedAgeYearsnumber | nullrequiredsource"ai" | "manual"requiredai: identified from a photo; manual: entered or imported.
priceSourcestring | nullrequiredWhere the price came from (retailer name).
priceSourceUrlstring | nullrequirednotesstring | nullrequiredphotoIdsstring[]requiredpho_ ids the item appears in, primary photo first.
valuationobjectrequiredvaluation.subtotalCentsintegerrequiredQuantity x unit cost, before tax.
valuation.taxCentsintegerrequiredSales tax at the claim's salesTaxBp.
valuation.rcvCentsintegerrequiredReplacement cost value: subtotal plus tax.
valuation.depreciationBpintegerrequiredDepreciation rate applied, in basis points (2500 = 25%).
valuation.depreciationCentsintegerrequiredvaluation.acvCentsintegerrequiredActual cash value: RCV less depreciation.
valuation.paymentCentsintegerrequiredWhat the claim pays for the line after any payment cap.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
curl -X POST 'https://contents.team/api/v1/items/{itemId}/replacements/{replacementId}/accept' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"quantity":1,"quantityConfirmed":true,"keepItemTitle":true}'Exports
Get an export
get/api/v1/exports/{exportId}
Status and progress. Once completed, downloads holds presigned URLs valid for 15 minutes; call again for fresh ones. Needs exports:write (whoever can start an export can collect it).
Scope: exports:write
Parameters
exportIdstring · pathrequiredexp_ id.
›Response 200Success
idstringrequiredexp_ id.
object"export"requiredclaimIdstringrequiredformat"pdf" | "xlsx" | "photo_zip"requiredstatus"queued" | "processing" | "completed" | "failed" | "expired" | "cancelled"requiredprogressintegerrequired0-100.
filenamestring | nullrequiredfileSizeBytesinteger | nullrequirederrorobject | nullrequirederror.codestringrequirederror.messagestringrequireddownloadsobject[]requiredEmpty until the export completes. A large photo ZIP comes in several parts.
downloads[].urlstringrequiredPresigned storage URL; GET it before expiresAt.
downloads[].filenamestringrequireddownloads[].bytesinteger | nullrequireddownloads[].expiresAtstring (date-time)requiredcreatedAtstring (date-time)requiredcompletedAtstring (date-time) | nullrequiredexpiresAtstring (date-time) | nullrequiredWhen the finished file is deleted; export again after that.
curl -X GET 'https://contents.team/api/v1/exports/{exportId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Start a report export
post/api/v1/claims/{claimId}/exports
Queues a PDF inventory report, an Excel inventory or a ZIP of the claim's photos, with the app's default options. Returns 202 with the export; poll GET /api/v1/exports/{exportId} until status is completed. At most 3 exports per organization run at once (429 export_concurrency_limit).
Scope: exports:write
Parameters
claimIdstring · pathrequiredclm_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
format"pdf" | "xlsx" | "photo_zip"requiredpdf = the inventory report; xlsx = the inventory spreadsheet; photo_zip = every photo.
›Response 202Success
idstringrequiredexp_ id.
object"export"requiredclaimIdstringrequiredformat"pdf" | "xlsx" | "photo_zip"requiredstatus"queued" | "processing" | "completed" | "failed" | "expired" | "cancelled"requiredprogressintegerrequired0-100.
filenamestring | nullrequiredfileSizeBytesinteger | nullrequirederrorobject | nullrequirederror.codestringrequirederror.messagestringrequireddownloadsobject[]requiredEmpty until the export completes. A large photo ZIP comes in several parts.
downloads[].urlstringrequiredPresigned storage URL; GET it before expiresAt.
downloads[].filenamestringrequireddownloads[].bytesinteger | nullrequireddownloads[].expiresAtstring (date-time)requiredcreatedAtstring (date-time)requiredcompletedAtstring (date-time) | nullrequiredexpiresAtstring (date-time) | nullrequiredWhen the finished file is deleted; export again after that.
curl -X POST 'https://contents.team/api/v1/claims/{claimId}/exports' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"format":"pdf"}'Webhooks
Get a webhook endpoint
get/api/v1/webhook-endpoints/{endpointId}
Scope: webhooks:manage
Parameters
endpointIdstring · pathrequiredwhe_ id.
›Response 200Success
idstringrequiredwhe_ id.
object"webhook_endpoint"requiredurlstringrequireddescriptionstring | nullrequiredevents"claim.created" | "photos.analysis_completed" | "items.priced" | "inventory.approved" | "export.ready"[]requiredmode"live" | "test"requiredReceives only events of this mode (the mode of the key that registered it).
status"enabled" | "disabled"requireddisabledReason"failing" | nullrequiredfailing: every delivery failed for three days. Register the URL again to resume.lastSuccessAtstring (date-time) | nullrequiredcreatedAtstring (date-time)required
curl -X GET 'https://contents.team/api/v1/webhook-endpoints/{endpointId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Delete a webhook endpoint
DEL/api/v1/webhook-endpoints/{endpointId}
Stops deliveries immediately, including pending retries.
Scope: webhooks:manage
Parameters
endpointIdstring · pathrequiredwhe_ id.
Response 204
No content
curl -X DELETE 'https://contents.team/api/v1/webhook-endpoints/{endpointId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"List webhook endpoints
get/api/v1/webhook-endpoints
The endpoints the calling key or OAuth app registered, in its mode, newest first (at most 20, one page). Endpoints registered in Settings or by another credential are not listed.
Scope: webhooks:manage
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequiredwhe_ id.
data[].object"webhook_endpoint"requireddata[].urlstringrequireddata[].descriptionstring | nullrequireddata[].events"claim.created" | "photos.analysis_completed" | "items.priced" | "inventory.approved" | "export.ready"[]requireddata[].mode"live" | "test"requiredReceives only events of this mode (the mode of the key that registered it).
data[].status"enabled" | "disabled"requireddata[].disabledReason"failing" | nullrequiredfailing: every delivery failed for three days. Register the URL again to resume.data[].lastSuccessAtstring (date-time) | nullrequireddata[].createdAtstring (date-time)requiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/webhook-endpoints' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Register a webhook endpoint
post/api/v1/webhook-endpoints
Events are POSTed as JSON with a Contents-Signature: t=<unix>,v1=<hex> header, HMAC-SHA256 of <t>.<raw body> keyed by the secret returned here (once). Failed deliveries retry with backoff for about three days; an endpoint that fails for three days is disabled. The endpoint receives events of the calling key's mode only. It belongs to the calling credential: only the same API key (or the same OAuth app connection) can list, read, test, replay or delete it through the API; others get a 404. Owners and admins see every endpoint in Settings.
Scope: webhooks:manage
Parameters
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
Request body
urlstringrequiredHTTPS URL on a public hostname. Private, loopback and link-local addresses are refused.
descriptionstringevents"claim.created" | "photos.analysis_completed" | "items.priced" | "inventory.approved" | "export.ready"[]requiredEvent types to receive.
›Response 201Success
idstringrequiredwhe_ id.
object"webhook_endpoint"requiredurlstringrequireddescriptionstring | nullrequiredevents"claim.created" | "photos.analysis_completed" | "items.priced" | "inventory.approved" | "export.ready"[]requiredmode"live" | "test"requiredReceives only events of this mode (the mode of the key that registered it).
status"enabled" | "disabled"requireddisabledReason"failing" | nullrequiredfailing: every delivery failed for three days. Register the URL again to resume.lastSuccessAtstring (date-time) | nullrequiredcreatedAtstring (date-time)requiredsecretstringrequiredwhsec_ signing secret. Shown only in this response: store it now.
curl -X POST 'https://contents.team/api/v1/webhook-endpoints' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"string","events":["claim.created"]}'List an endpoint's deliveries
get/api/v1/webhook-endpoints/{endpointId}/deliveries
Newest first, with the status, attempt count and the start of your endpoint's last response.
Scope: webhooks:manage
Parameters
endpointIdstring · pathrequiredwhe_ id.
limitinteger · queryPage size, 1-100.
cursorstring · querynextCursorfrom the previous page.
›Response 200Success
object"list"requireddataobject[]requireddata[].idstringrequiredwhd_ id.
data[].object"webhook_delivery"requireddata[].endpointIdstringrequiredwhe_ id.
data[].eventIdstringrequiredevt_ id; also the
idin the event body.data[].eventTypestringrequireddata[].status"pending" | "sending" | "succeeded" | "failed"requireddata[].attemptsintegerrequireddata[].responseCodeinteger | nullrequireddata[].responseSnippetstring | nullrequiredFirst 1 KB of your endpoint's last response.
data[].errorstring | nullrequireddata[].nextAttemptAtstring (date-time) | nullrequireddata[].lastAttemptAtstring (date-time) | nullrequireddata[].deliveredAtstring (date-time) | nullrequireddata[].createdAtstring (date-time)requiredhasMorebooleanrequirednextCursorstring | nullrequiredPass as
cursorfor the next page; null on the last page.
curl -X GET 'https://contents.team/api/v1/webhook-endpoints/{endpointId}/deliveries' \
-H "Authorization: Bearer $CONTENTS_API_KEY"Replay a delivery
post/api/v1/webhook-endpoints/{endpointId}/deliveries/{deliveryId}/replay
Sends the same event (same id, same body) again now, with a fresh retry budget. Deduplicate on the event id if you may have processed it already.
Scope: webhooks:manage
Parameters
endpointIdstring · pathrequiredwhe_ id.
deliveryIdstring · pathrequiredwhd_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
›Response 202Success
idstringrequiredwhd_ id.
object"webhook_delivery"requiredendpointIdstringrequiredwhe_ id.
eventIdstringrequiredevt_ id; also the
idin the event body.eventTypestringrequiredstatus"pending" | "sending" | "succeeded" | "failed"requiredattemptsintegerrequiredresponseCodeinteger | nullrequiredresponseSnippetstring | nullrequiredFirst 1 KB of your endpoint's last response.
errorstring | nullrequirednextAttemptAtstring (date-time) | nullrequiredlastAttemptAtstring (date-time) | nullrequireddeliveredAtstring (date-time) | nullrequiredcreatedAtstring (date-time)required
curl -X POST 'https://contents.team/api/v1/webhook-endpoints/{endpointId}/deliveries/{deliveryId}/replay' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"Send a test event
post/api/v1/webhook-endpoints/{endpointId}/test
Queues a webhook.ping event to this endpoint only and returns its delivery; poll the delivery log to see your endpoint's response.
Scope: webhooks:manage
Parameters
endpointIdstring · pathrequiredwhe_ id.
Idempotency-Keystring · headerrequiredRequired on every POST (a UUID works). Retrying with the same key and body returns the first successful response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is a 409 idempotency_key_reused. Errors are not stored, so a failed request can be retried with the same key. Keys are per organization and expire after 24 hours.
›Response 202Success
idstringrequiredwhd_ id.
object"webhook_delivery"requiredendpointIdstringrequiredwhe_ id.
eventIdstringrequiredevt_ id; also the
idin the event body.eventTypestringrequiredstatus"pending" | "sending" | "succeeded" | "failed"requiredattemptsintegerrequiredresponseCodeinteger | nullrequiredresponseSnippetstring | nullrequiredFirst 1 KB of your endpoint's last response.
errorstring | nullrequirednextAttemptAtstring (date-time) | nullrequiredlastAttemptAtstring (date-time) | nullrequireddeliveredAtstring (date-time) | nullrequiredcreatedAtstring (date-time)required
curl -X POST 'https://contents.team/api/v1/webhook-endpoints/{endpointId}/test' \
-H "Authorization: Bearer $CONTENTS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"Meta
The calling credential and its organization
get/api/v1/me
Use it to check a key works: returns the org, live/test mode and the key's scopes. Needs no scope.
›Response 200Success
object"me"requiredorganizationobjectrequiredorganization.idstringrequiredorg_ id.
organization.namestringrequiredmode"live" | "test"requiredcredentialobjectrequiredcredential.type"api_key" | "oauth"requiredcredential.namestringrequiredThe API key name, or the OAuth app name.
credential.prefixstringrequiredFirst characters of the key, as Settings shows it; "cto_" for an OAuth token.
credential.scopes"claims:read" | "claims:write" | "photos:read" | "photos:write" | "items:read" | "exports:write" | "webhooks:manage" | "items:write"[]requiredcredential.createdAtstring (date-time)requiredcredential.lastUsedAtstring (date-time) | nullrequiredcredential.expiresAtstring (date-time) | nullrequired
curl -X GET 'https://contents.team/api/v1/me' \
-H "Authorization: Bearer $CONTENTS_API_KEY"This OpenAPI document
get/api/v1/openapi.json
Also served as YAML at /openapi/v1.yaml. No authentication.
›Response 200Success
openapistringrequired
curl -X GET 'https://contents.team/api/v1/openapi.json' \
-H "Authorization: Bearer $CONTENTS_API_KEY"