API reference / Collectors
Scanning books in: captures (barcodes, cover and slab photos, search picks), matching, fixing the match, and adding the copy.
/api/v1/scansFrom a barcode decode, a photo capture (upload it, then process the scan) or a pick from search. Idempotent on clientKey: a retried capture returns the scan it already made. Barcode and manual scans are matched right away when process is true. 240 a minute.
collector.createScan()scans:write(API keys are coming soon)Idempotency-KeyX-Request-Idobject
clientKeystringrequired8–80 charactersbatchIdstring (uuid) or nulloptionalmodeenumrequiredOne ofbarcodecoverslabmanual
intentenumoptionalOne ofownedwishlist
barcodeobject or nulloptionaltextstringrequired6–40 charactersaddOnstring or nulloptionalformatstring or nulloptionalat most 20 charactersaddOnMissingbooleanoptionalslabQrstring or nulloptionalat most 600 characterscoverHashstring or nulloptionaldevicestring or nulloptionalat most 40 charactersselectionobject or nulloptionalissueIdstring (uuid) or nullrequirededitionIdstring (uuid) or nullrequiredproviderobject or nullrequiredproviderenumrequiredOne ofmetrongcdcataloggd
issueIdstringrequired1–262 charactersvariantIdstring or nulloptionalat most 200 charactersseriesIdstring (uuid) or nullrequiredseriesTitlestringrequired1–300 charactersseriesStartYearinteger or nullrequired1800–2200seriesVolumeinteger or nullrequired0–999publisherstring or nullrequiredat most 200 charactersnumberstringrequired1–40 charactersissueTitlestring or nullrequiredat most 300 characterscoverDatestring or nullrequiredat most 40 characterscoverUrlstring (uri) or nullrequiredat most 2000 characterseditionNamestring or nullrequiredat most 300 charactersreleaseLabelstring or nulloptionalat most 400 characterscoverVariantstring or nullrequiredat most 120 charactersprintinginteger or nullrequired1–99upcstring or nullrequiredat most 20 charactersupcAddonstring or nullrequiredat most 5 charactersisKeybooleanrequiredscorenumberrequired0–1reasonsarray of stringrequiredat most 12 itemssourceenumrequiredOne ofbarcodecovermemorysearchmanualprovisional
exactbooleanrequiredownedCopiesintegerrequiredat least 0selectedbooleanoptionaleditionUnknownbooleanoptionalpendingVariantobject or nulloptionalidstring (uuid)requiredlabelstringrequiredat most 300 charactersprocessbooleanoptionalobject
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_idempotency_key: The Idempotency-Key isn’t 1–255 visible ASCII characters.
invalid_barcode: The barcode isn’t a UPC, EAN or ISBN.
invalid_candidate: The picked candidate doesn’t name a book.
invalid_cert: The slab QR code’s cert number isn’t one.
unauthorized: Not signed in (no session or token, or an expired one).
batch_not_found: No open session of yours with that batchId.
idempotency_in_progress: The first request with this key is still running.
idempotency_key_reused: This key was used for a different request.
rate_limited: Scanning faster than 240 a minute; wait the Retry-After seconds.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.createScan({ body: { … } });/api/v1/scans/{id}Removes the scan and its photos. A copy it was added as stays in the collection.
collector.deleteScan()scans:write(API keys are coming soon)X-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
object
ok"true"requiredunauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
internal: Something went wrong on our side (quote the requestId).
curl -X DELETE 'https://collection.id/api/v1/scans/<id>' \
-H "Authorization: Bearer $COLLECTOR_TOKEN"await collector.deleteScan({ path: { id } });/api/v1/scans/{id}Pick a candidate (select: { index }, or a search result as select: { candidate }; null clears the pick), choose the edition, edit the copy draft, change the intent, skip it, or restore a skipped scan.
collector.updateScan()scans:write(API keys are coming soon)X-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
object
selectobject or object or nulloptionalOption 1: object
indexintegerrequired0–20Option 2: object
candidateobjectrequiredissueIdstring (uuid) or nullrequirededitionIdstring (uuid) or nullrequiredproviderobject or nullrequiredproviderenumrequiredOne ofmetrongcdcataloggd
issueIdstringrequired1–262 charactersvariantIdstring or nulloptionalat most 200 charactersseriesIdstring (uuid) or nullrequiredseriesTitlestringrequired1–300 charactersseriesStartYearinteger or nullrequired1800–2200seriesVolumeinteger or nullrequired0–999publisherstring or nullrequiredat most 200 charactersnumberstringrequired1–40 charactersissueTitlestring or nullrequiredat most 300 characterscoverDatestring or nullrequiredat most 40 characterscoverUrlstring (uri) or nullrequiredat most 2000 characterseditionNamestring or nullrequiredat most 300 charactersreleaseLabelstring or nulloptionalat most 400 characterscoverVariantstring or nullrequiredat most 120 charactersprintinginteger or nullrequired1–99upcstring or nullrequiredat most 20 charactersupcAddonstring or nullrequiredat most 5 charactersisKeybooleanrequiredscorenumberrequired0–1reasonsarray of stringrequiredat most 12 itemssourceenumrequiredOne ofbarcodecovermemorysearchmanualprovisional
exactbooleanrequiredownedCopiesintegerrequiredat least 0selectedbooleanoptionaleditionUnknownbooleanoptionalpendingVariantobject or nulloptionalidstring (uuid)requiredlabelstringrequiredat most 300 charactersdraftobjectoptionalgradenumber or nulloptional0.5–10grading_companyenum or nulloptionalOne ofcgccbcspsapgxegshgaother
cert_numberstring or nulloptionalat most 64 characterslabel_typestring or nulloptionalat most 40 characterspage_qualityenum or nulloptionalOne ofwhiteoff_white_to_whiteoff_whitecream_to_off_whitecreamlight_tantanbrittle
is_signedbooleanoptionalsignaturesarray of stringoptionalat most 12 itemssignature_authenticationenum or nulloptionalOne ofcgc_signature_seriescbcs_verifiedpsa_dnacoawitnessedunverified
purchase_price_centsinteger or nulloptional0–1000000000acquired_onstring or nulloptionallocationstring or nulloptionalat most 120 characterstagsarray of stringoptionalat most 30 itemsnotesstring or nulloptionalat most 8000 charactersintentenumoptionalOne ofownedwishlist
skipbooleanoptionalrestorebooleanoptionaleditionobject or object or "unknown"optionalOption 1: object
editionIdstring (uuid)requiredOption 2: object
pendingVariantIdstring (uuid)requiredOption 3: "unknown"
object
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_candidate: The picked candidate doesn’t name a book.
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
already_added: The scan was already added as a copy.
internal: Something went wrong on our side (quote the requestId).
curl -X PATCH 'https://collection.id/api/v1/scans/<id>' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.updateScan({ path: { id }, body: { … } });/api/v1/scans/{id}/addAdds the chosen (or top) candidate as a copy, with the session’s defaults and the scan’s draft. The body is optional: index adds another candidate, draft sets copy fields. Adding twice answers the same copy.
collector.addScan()scans:writecollection:write(API keys are coming soon)Idempotency-KeyX-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
object
indexintegeroptional0–20draftobjectoptionalgradenumber or nulloptional0.5–10grading_companyenum or nulloptionalOne ofcgccbcspsapgxegshgaother
cert_numberstring or nulloptionalat most 64 characterslabel_typestring or nulloptionalat most 40 characterspage_qualityenum or nulloptionalOne ofwhiteoff_white_to_whiteoff_whitecream_to_off_whitecreamlight_tantanbrittle
is_signedbooleanoptionalsignaturesarray of stringoptionalat most 12 itemssignature_authenticationenum or nulloptionalOne ofcgc_signature_seriescbcs_verifiedpsa_dnacoawitnessedunverified
purchase_price_centsinteger or nulloptional0–1000000000acquired_onstring or nulloptionallocationstring or nulloptionalat most 120 characterstagsarray of stringoptionalat most 30 itemsnotesstring or nulloptionalat most 8000 charactersobject
itemIdstring (uuid)requiredThe copy in the collection.
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_idempotency_key: The Idempotency-Key isn’t 1–255 visible ASCII characters.
invalid_candidate: The chosen candidate doesn’t name a book.
invalid_copy: The copy fields aren’t valid together.
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
idempotency_in_progress: The first request with this key is still running.
no_match: Nothing to add: the scan matched no book.
still_matching: The scan is still being matched; try again shortly.
idempotency_key_reused: This key was used for a different request.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans/<id>/add' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.addScan({ path: { id }, body: { … } });/api/v1/scans/{id}/processMatches (or re-matches) a scan: a barcode lookup, then an AI read of the cover or slab photo when one was uploaded and the barcode didn’t settle it. The body is optional: the uploaded imagePath and thumbPath, a typed addOn, coverHash, mode, or rereadPhoto: true. 30 a minute.
collector.processScan()scans:write(API keys are coming soon)X-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
object
imagePathstring or nulloptionalat most 400 charactersthumbPathstring or nulloptionalat most 400 charactersaddOnstring or nulloptionalcoverHashstring or nulloptionalmodeenumoptionalOne ofbarcodecoverslab
rereadPhotobooleanoptionalobject
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_addon: The add-on isn’t 2 or 5 digits.
no_barcode: An add-on was sent for a scan without a barcode.
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
already_added: The scan was already added as a copy.
already_processing: Another match of this scan is running.
rate_limited: Matching faster than 30 a minute; wait the Retry-After seconds.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans/<id>/process' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.processScan({ path: { id }, body: { … } });/api/v1/scans/{id}/provisionalFor a book no source knows yet: creates a provisional catalog issue and picks it for this scan. Catalog moderators review it before it’s everyone’s.
collector.createProvisionalForScan()scans:write(API keys are coming soon)X-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
object
seriesTitlestringrequired1–300 charactersnumberstringrequired1–40 characterspublisherstring or nulloptionalat most 200 charactersstartYearinteger or nulloptional1800–2200coverDatestring or nulloptionalobject
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
already_added: The scan was already added.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans/<id>/provisional' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.createProvisionalForScan({ path: { id }, body: { … } });/api/v1/scans/{id}/searchCatalog candidates for fixing a scan’s match, ranked with this scan’s evidence (its barcode and photo reads). 120 searches a minute.
collector.searchForScan()scans:write(API keys are coming soon)If-None-MatchX-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The scan’s id. |
qrequired | query | string | 1–200 characters |
object
invalid_request: The query or body doesn’t match the schema (see errors[]).
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
scan_not_found: No scan of yours with that id.
rate_limited: Searching faster than 120 a minute; wait the Retry-After seconds.
internal: Something went wrong on our side (quote the requestId).
curl 'https://collection.id/api/v1/scans/<id>/search' \
-H "Authorization: Bearer $COLLECTOR_TOKEN"await collector.searchForScan({ path: { id }, query: { … } });/api/v1/scans/batchesA session (batch) groups a sitting’s scans and applies copy defaults (status, location, price, tags) to every copy added from it. reuseOpen: true returns your open session instead, so a paired phone and the desktop share one tray.
collector.createScanBatch()scans:write(API keys are coming soon)Idempotency-KeyX-Request-Idobject
namestring or nulloptionalat most 120 charactersdefaultsobjectoptionalstatusenumoptionalOne ofownedwishlist
locationstring or nulloptionalat most 120 charactersacquired_onstring or nulloptionalpurchase_price_centsinteger or nulloptional0–1000000000purchase_currencystringoptionalacquired_fromstring or nulloptionalat most 120 characterstagsarray of stringoptionalat most 30 itemsreuseOpenbooleanoptionalobject
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_idempotency_key: The Idempotency-Key isn’t 1–255 visible ASCII characters.
unauthorized: Not signed in (no session or token, or an expired one).
idempotency_in_progress: The first request with this key is still running.
idempotency_key_reused: This key was used for a different request.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans/batches' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.createScanBatch({ body: { … } });/api/v1/scans/batches/{id}Rename it, change its copy defaults, or close it.
collector.updateScanBatch()scans:write(API keys are coming soon)X-Request-Id| Name | In | Type | About |
|---|---|---|---|
idrequired | path | string (uuid) | The session’s id. |
object
namestring or nulloptionalat most 120 charactersdefaultsobjectoptionalstatusenumoptionalOne ofownedwishlist
locationstring or nulloptionalat most 120 charactersacquired_onstring or nulloptionalpurchase_price_centsinteger or nulloptional0–1000000000purchase_currencystringoptionalacquired_fromstring or nulloptionalat most 120 characterstagsarray of stringoptionalat most 30 itemsclosebooleanoptionalobject
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
unauthorized: Not signed in (no session or token, or an expired one).
not_found: Nothing at that address.
batch_not_found: No session of yours with that id.
internal: Something went wrong on our side (quote the requestId).
curl -X PATCH 'https://collection.id/api/v1/scans/batches/<id>' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.updateScanBatch({ path: { id }, body: { … } });/api/v1/scans/bulkAdds, skips or deletes up to 100 scans at once (“Add all matched”). Each scan answers on its own: one failing doesn’t stop the rest.
collector.bulkScans()scans:writecollection:write(API keys are coming soon)Idempotency-KeyX-Request-Idobject
actionenumrequiredOne ofaddskipdelete
idsarray of string (uuid)required1–100 itemsobject
resultsarray of objectrequiredidstring (uuid)requiredokbooleanrequireditemIdstring (uuid)optionalThe copy, for an added scan.
errorstringoptionalWhy this one failed (the error code).
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
invalid_idempotency_key: The Idempotency-Key isn’t 1–255 visible ASCII characters.
unauthorized: Not signed in (no session or token, or an expired one).
idempotency_in_progress: The first request with this key is still running.
idempotency_key_reused: This key was used for a different request.
internal: Something went wrong on our side (quote the requestId).
curl -X POST 'https://collection.id/api/v1/scans/bulk' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.bulkScans({ body: { … } });/api/v1/scans/searchCatalog candidates for “Type it” (no scan yet), with the copies you own and your wishlist entries for each. Up to 16. Shares the 120-a-minute search allowance.
collector.searchToScan()catalog:read(API keys are coming soon)If-None-MatchX-Request-Id| Name | In | Type | About |
|---|---|---|---|
qrequired | query | string | 1–200 characters |
object
invalid_request: The query or body doesn’t match the schema (see errors[]).
unauthorized: Not signed in (no session or token, or an expired one).
rate_limited: Searching faster than 120 a minute; wait the Retry-After seconds.
internal: Something went wrong on our side (quote the requestId).
curl 'https://collection.id/api/v1/scans/search' \
-H "Authorization: Bearer $COLLECTOR_TOKEN"await collector.searchToScan({ query: { … } });/api/v1/scans/upload-urlSigned upload URLs (valid 2 hours) for the scan’s photo and its thumbnail. Upload each with Supabase Storage’s uploadToSignedUrl(path, token, blob) on the scans bucket, then process the scan with the paths.
collector.createScanUploadUrl()scans:write(API keys are coming soon)X-Request-Idobject
scanIdstring (uuid)requiredobject
bucket"scans"requiredimageobjectrequiredpathstringrequiredscans/{uid}/{scanId}/{uuid}.jpg in the scans bucket.
tokenstringrequiredThe upload token (Supabase Storage uploadToSignedUrl).
signedUrlstring (uri)requiredValid 2 hours.
thumbobjectrequiredpathstringrequiredscans/{uid}/{scanId}/{uuid}.jpg in the scans bucket.
tokenstringrequiredThe upload token (Supabase Storage uploadToSignedUrl).
signedUrlstring (uri)requiredValid 2 hours.
invalid_request: The query or body doesn’t match the schema (see errors[]).
invalid_json: The body isn’t JSON.
unauthorized: Not signed in (no session or token, or an expired one).
scan_not_found: No scan of yours with that id.
storage_quota: Your photo storage is full.
internal: Something went wrong on our side (quote the requestId).
upload_url_failed: Storage couldn’t sign the upload; try again.
curl -X POST 'https://collection.id/api/v1/scans/upload-url' \
-H "Authorization: Bearer $COLLECTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d @body.jsonawait collector.createScanUploadUrl({ body: { … } });