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.
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 | nullrequireddata[].itemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
data[].approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included.
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 | nullrequireditemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included.
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 | nullrequireditemCountintegerrequiredInventory lines, excluding auto-detected duplicates.
approvedRcvCentsintegerrequiredReplacement cost of approved items, tax included.
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.photosQueuedintegerrequiredanalysis.photosStagedintegerrequiredanalysis.photosAnalyzingintegerrequiredanalysis.photosCompletedintegerrequiredanalysis.photosFailedintegerrequired
curl -X GET 'https://contents.team/api/v1/claims/{claimId}' \
-H "Authorization: Bearer $CONTENTS_API_KEY"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.
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.
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.
descriptionstringrequiredretailerDescriptionstring | 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. 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 (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 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.
descriptionstringrequiredretailerDescriptionstring | 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 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[].descriptionstringrequireddata[].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 (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.
descriptionstringrequiredretailerDescriptionstring | 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.
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"]}'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.
descriptionstringrequiredretailerDescriptionstring | 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"