ClearList Listings API
Create, read, update, delete, and bulk-create items.
Create, read, update, delete, and bulk-create items.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/clearlist-me:clearlist-me-listings-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: ClearList Listings API
version: 0.3.6
description: 'API for ClearList, an AI resale manager. Agents authenticate via an API key acquired through the email verification flow, or as an OAuth 2.1 client (see /auth.md). Standard response envelope: `{ success: boolean, data?: object, error?: string }`. Errors return the Error schema with a machine-readable `code` and a resolution `hint`. Versioning: every response carries an X-API-Version header (currently 0.3); breaking changes ship as a new version and the old behavior is kept through a deprecation window announced via Deprecation and Sunset headers (RFC 8594) and the changelog at /developers. Retries: the operations that declare the Idempotency-Key header parameter accept it (1-255 chars; a UUID works well). The first successful (2xx) response is stored for 24h, and a retry with the same key replays it instead of duplicating the side effect, marked by an Idempotency-Replayed: true response header. An error returned by the operation itself is never stored and releases the key, so retrying that same request re-executes it. A key is scoped to the seller and the operation, and it identifies ONE request: while its record is live (the first request is still running, or it succeeded within the last 24h), the same key on the same operation with a different request body (compared byte for byte, so reformatted JSON counts as different) or from a different kind of credential (a signed-in session, an API key, and an OAuth token are three kinds) is refused with 422 `idempotency_key_reuse`. That refusal leaves the original record in place, so a corrected request needs a NEW key. A key outside the allowed characters is refused with 400 `invalid_idempotency_key`. Do not assume an operation is retry-safe because it looks like an update: check for the parameter on the operation itself.'
contact:
email: hello@clearlist.me
url: https://clearlist.me
servers:
- url: https://clearlist.me
description: Production
security:
- apiKey: []
- oauth2: []
tags:
- name: Listings
description: Create, read, update, delete, and bulk-create items.
paths:
/api/items:
get:
operationId: getListings
x-tool-id: get_listings
tags:
- Listings
summary: List items
description: 'Returns the authenticated seller''s items. Soft-deleted items (tombstones) are hidden unless `include_deleted=true`, which is the only way to recover an item_id for POST /api/items/{id}/restore after the deleting session is gone. Tombstone rows are enriched with `restorable` (boolean), `restorable_until` (ISO timestamp — the field to trust) and `restore_hours_remaining` (countdown, stale the moment it is read); tombstones past the window still appear, flagged `restorable: false`, until the purge collects them.'
parameters:
- name: include_deleted
in: query
required: false
schema:
type: string
enum:
- 'true'
- 'false'
description: Set to "true" to include soft-deleted items. Any other non-empty value is a 400, never silently treated as false — a silently-dropped tombstone reads as "this listing never existed" to the caller.
responses:
'200':
description: Items list.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
type: object
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
post:
operationId: createListing
security:
- apiKey: []
- oauth2:
- write
x-tool-id: create_listing
tags:
- Listings
summary: Create a listing
description: Upload photos and create an AI-generated listing. Supports Idempotency-Key for safe retries.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- photos
properties:
photos:
type: array
items:
type: string
format: uri
maxItems: 5
responses:
'200':
description: 'Listing created — but check `data.inactive`. An over-the-active-limit item is NOT refused: it is saved with `status: ''inactive''` and the response carries `inactive: true`. An inactive item is not on the sale page, so reporting it as a live listing is wrong; tell the seller it is saved but needs an upgrade (or a freed slot) to go live.'
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
inactive:
type: boolean
description: Present and true only when the item was saved as status 'inactive' because the active-item limit is reached.
additionalProperties: true
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 'Creation ceiling reached for the current tier (active-item limit or lifetime cap). The error message names the limit; the fix is an upgrade via POST /api/payments/checkout-link. Also `code: ADDRESS_IN_PUBLIC_LISTING` when an API key or OAuth token writes the seller''s stored street address into public listing text (title, description, transport_notes, condition, category, or specifications). Nothing is saved. Remove the address; to give it to a buyer, use the address share flow. A signed-in seller''s own session is not checked.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'This account has no email on file, so an assistant cannot publish a listing (agent credentials only). Nothing was saved. Not retryable until the account has an email. Also returned with `code: idempotency_conflict` while a request with the same Idempotency-Key is still being processed; that one is worth retrying with the same key after a few seconds. A 409 without that code is the operation''s own refusal: read `error`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
$ref: '#/components/responses/IdempotencyKeyReuse'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
/api/items/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
operationId: getItem
tags:
- Listings
summary: Get one item
responses:
'200':
description: Item.
content:
application/json:
schema:
type: object
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/ServerError'
put:
operationId: editListing
security:
- apiKey: []
- oauth2:
- write
x-tool-id: edit_listing
tags:
- Listings
summary: Update a listing
description: 'Update any field on an item (price, title, description, status, etc.). This route does NOT accept an Idempotency-Key (check the parameters of an operation before sending one — this spec declares the header only on the six routes that honour it). Retry safety comes from the conflict check instead: on a timeout, re-read the item with GET and re-apply the change, and treat the "changed while you were editing" 409 below as your signal that a concurrent write landed first.'
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
'200':
description: Updated. The body carries the full updated item document.
content:
application/json:
schema:
type: object
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 'Either the credential does not own this item (the item exists and belongs to another seller; never retry, no key rotation or backoff changes the answer), or `code: ADDRESS_IN_PUBLIC_LISTING` when an API key or OAuth token writes the seller''s stored street address into public listing text (title, description, transport_notes, condition, category, or specifications). Nothing is saved. Remove the address; to give it to a buyer, use the address share flow. A signed-in seller''s own session is not checked.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: 'Three distinct conflicts share this status; read `error` to tell them apart.
1. **The item is deleted.** `error` is "This item is deleted. Restore it first: POST /api/items/{id}/restore". An edit cannot resurrect a tombstone, because that would skip the active-item capacity re-check that POST /api/items/{id}/restore runs in a transaction. Call restore, then retry the edit.
2. **The item changed while you were editing it.** `error` is "This item changed while you were editing it. Reload and try again." Your write was rejected rather than applied on top of someone else''s. Re-read the item with GET and re-apply your change to the fresh document.
3. **The seller account backing the item is missing or incomplete.** `error` is "Cannot update: this seller account is missing or incomplete." An account-level inconsistency, not an edit conflict — retrying cannot fix it; surface it to the seller.
None is retryable as-is: the same request repeated returns the same 409.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/ServerError'
delete:
operationId: deleteListing
security:
- apiKey: []
- oauth2:
- write
x-tool-id: delete_listing
tags:
- Listings
summary: Soft-delete a listing
description: Soft delete. The listing is hidden from the sale page, the dashboard, and every status-filtered read, but the document and its photos survive for 7 days and can be recovered with POST /api/items/{id}/restore. A purge job hard-deletes the document and its Storage blobs once the window closes, after which restore returns 410. Repeating the delete is a no-op success, not a 404.
responses:
'200':
description: Deleted, or already deleted.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
item_id:
type: string
deleted:
type: boolean
description: Always true on a 200.
already_deleted:
type: boolean
description: 'Present and true only when the item was ALREADY a tombstone, so this call changed nothing. Absent on the call that performed the delete. Treat it as success either way, but do not report a fresh deletion to the user when it is set, and do not use it to infer that the restore window just started: the 7-day clock runs from the ORIGINAL delete, which may be nearly expired.'
'400':
description: The item has an active reservation. A buyer is holding it, so deleting would strand them. `error` is "This item has an active reservation. Deactivate the item first to cancel reservations, then delete it." Set the item's status to `inactive` via PUT (which cancels the reservation and notifies the buyer), then delete.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/ServerError'
/api/items/{id}/restore:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
operationId: restoreListing
security:
- apiKey: []
- oauth2:
- write
x-tool-id: restore_listing
tags:
- Listings
summary: Restore a soft-deleted listing
description: 'Undoes DELETE /api/items/{id} inside the 7-day window. The item returns to the status it held before the delete, except that a previously `reserved` item comes back `available` (the reservation that justified it is long gone) and an item that no longer fits the seller''s active-item limit comes back `inactive` rather than being refused. Pass an Idempotency-Key: this operation is NOT self-idempotent, and a retry without one reports 409 "This item is not deleted" for a restore that in fact succeeded.'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: Restored.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
item_id:
type: string
status:
type: string
description: The status the item was restored TO. Not necessarily the status it had before the delete.
enum:
- available
- taken
- inactive
downgraded_to_inactive:
type: boolean
description: 'True when the seller was at their active-item limit, so the item was restored as `inactive` instead of the status it had before the delete. The restore succeeded and the seller''s work is back, but the listing is NOT visible on the sale page. Surface this: reporting a plain success here leaves the seller believing a hidden item is live. `message` carries seller-facing wording naming the limit.'
message:
type: string
description: Present only when downgraded_to_inactive is true.
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: 'Nothing to restore. Either the item is not deleted (`error` is "This item is not deleted") or the seller account that owns it is missing or incomplete. Not retryable: repeating the call returns the same 409. If you sent an Idempotency-Key on the original attempt, retry with that same key instead. A 409 here after a request that timed out usually means the first attempt committed and you are seeing the follow-up call, not a failure. Also returned with `code: idempotency_conflict` while a request with the same Idempotency-Key is still being processed; that one is worth retrying with the same key after a few seconds. A 409 without that code is the operation''s own refusal: read `error`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'410':
description: The 7-day restore window has passed. `error` is "The restore window for this item has passed." The listing and its photos are unrecoverable; there is no appeal path and no retry. Refused on the window rather than on the document still existing, so the answer does not depend on when the purge job last ran.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
$ref: '#/components/responses/IdempotencyKeyReuse'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
/api/items/bulk-upload:
post:
operationId: bulkCreateListings
security:
- apiKey: []
- oauth2:
- write
x-tool-id: bulk_create_listings
tags:
- Listings
summary: Upload a photo batch (step 1 of bulk creation)
description: 'Uploads up to 50 base64 photos to Storage and returns their URLs plus a batchId — synchronously, HTTP 200, no AI. This is step 1 of the bulk chain: bulk-upload (photos → URLs) → POST /api/items/bulk-generate (URLs → AI listings via job polling) → POST /api/items (save). It does NOT group photos or generate listings itself. Supports Idempotency-Key for safe retries.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- photos
properties:
photos:
type: array
items:
type: string
description: 'Base64-encoded photo, with or without a data: URL prefix. Not a URL.'
maxItems: 50
responses:
'200':
description: Photos stored.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
fullUrls:
type: array
items:
type: string
format: uri
thumbnailUrls:
type: array
items:
type: string
format: uri
batchId:
type: string
count:
type: number
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
description: 'A photo is still uploading (sent with Retry-After: 5). Retry after the delay. Also returned with `code: idempotency_conflict` while a request with the same Idempotency-Key is still being processed; that one is worth retrying with the same key after a few seconds. A 409 without that code is the operation''s own refusal: read `error`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
$ref: '#/components/responses/IdempotencyKeyReuse'
'429':
$ref: '#/components/responses/RateLimited'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
/api/items/bulk-generate:
post:
operationId: generateListingFromPhotos
security:
- apiKey: []
- oauth2:
- write
tags:
- Listings
summary: Generate one listing from photos (async)
description: 'Runs AI generation over up to 10 photos of a SINGLE item (already-uploaded Storage URLs, not base64) and returns a job_id immediately; poll GET /api/ai-jobs/{jobId} for the listing. Pass an Idempotency-Key: a replay returns the ORIGINAL job_id, so the retry polls the generation already in flight instead of paying for a second one.'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- photoUrls
properties:
photoUrls:
type: array
items:
type: string
format: uri
minItems: 1
maxItems: 10
description: Photos of one item, different angles. Only the first 5 are sent to the model.
sellerContext:
type: string
description: Free-text notes from the seller. Photo evidence wins on conflict unless isCorrection is true.
isCorrection:
type: boolean
description: True when sellerContext is an explicit correction of a previous result, which makes the model defer to the seller instead of the photo.
groupLabel:
type: string
isBundle:
type: boolean
bundleComponents:
type: array
items:
type: string
responses:
'200':
description: Job accepted (200, not 202). `status` is "processing" and `job_id` is the handle to poll.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
status:
type: string
enum:
- processing
job_id:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/IdempotencyConflict'
'422':
$ref: '#/components/responses/IdempotencyKeyReuse'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
components:
headers:
WWWAuthenticate:
schema:
type: string
description: 'RFC 9728 challenge: `Bearer resource_metadata="https://clearlist.me/.well-known/oauth-protected-resource", error="invalid_token"`.'
RateLimitRemaining:
schema:
type: integer
description: Requests remaining in the current window.
RateLimitLimit:
schema:
type: integer
description: Request quota for the current window.
RateLimitReset:
schema:
type: integer
description: Unix epoch seconds when the window resets.
responses:
NotFound:
description: Resource (or API route) not found. Returned as JSON, never HTML.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: Too many requests.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
X-RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
X-RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
IdempotencyConflict:
description: '`code: idempotency_conflict`. A request with this Idempotency-Key is still being processed. Retry the same request with the same key after a few seconds: if the first request succeeded the retry replays its response; if it failed, or has been pending for over a minute, the retry runs the operation again.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
IdempotencyKeyReuse:
description: '`code: idempotency_key_reuse`. While an earlier request with this Idempotency-Key is still running or succeeded within the last 24h, the key was sent to the same operation with a different body (compared byte for byte) or from a different kind of credential (signed-in session, API key, or OAuth token). Nothing was executed and the original record is untouched, so retrying with this key repeats the 422. Send the request with a new key.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ServerError:
description: Unexpected server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Missing or invalid API key.
headers:
WWW-Authenticate:
$ref: '#/components/headers/WWWAuthenticate'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid or missing parameters.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: 'Authenticated, but the credential does not own this resource. Distinct from 404: the resource exists and belongs to another seller. Never retry: no key rotation or backoff changes the answer.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
description: 'Unique client-generated key (1-255 chars from A-Za-z0-9_-:.; a UUID works well), scoped to the seller and the operation. The first successful (2xx) response is stored for 24h; retrying the identical request with the same key replays it instead of re-executing, so network-failure retries never create duplicates. A duplicate that arrives while the first is still running gets 409 idempotency_conflict. The same key with a different body (byte for byte) or a different kind of credential gets 422 idempotency_key_reuse and leaves the original record in place: send a new key. An error the operation itself returns is never stored and releases the key.'
schema:
type: string
minLength: 1
maxLength: 255
pattern: ^[A-Za-z0-9_:.-]+$
schemas:
Error:
type: object
required:
- success
- error
properties:
success:
type: boolean
example: false
error:
type: string
description: Human-readable error message.
code:
type: string
description: Machine-readable error code, e.g. route_not_found, invalid_token, rate_limited.
message:
type: string
description: Detailed message.
hint:
type: string
description: Resolution guidance for agents.
retryable:
type: boolean
description: Present on refusals that are worth retrying unchanged. Currently only a wrong-conversation address confirmation, where the token is untouched and only the id was wrong. Absent means terminal.
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-ClearList-API-Key
description: 'API key in `cl_<64-hex>` format. Obtained via POST /api/auth/verify-code with `{ email, code, agent: true }`. Always full access (read and write). The same credential is also accepted as `Authorization: Bearer cl_<64-hex>`.'
oauth2:
type: oauth2
description: 'OAuth 2.1 authorization code flow. PKCE with code_challenge_method=S256 is required; public clients only (token_endpoint_auth_method none). Access tokens are `cl_<64-hex>` and are sent as `Authorization: Bearer <token>` (X-ClearList-API-Key also works). Operations that change state require the `write` scope; a grant without it gets 403. Reading needs no specific scope, so a `write`-only grant can also read.'
flows:
authorizationCode:
authorizationUrl: https://clearlist.me/oauth/authorize
tokenUrl: https://clearlist.me/api/oauth/token
refreshUrl: https://clearlist.me/api/oauth/token
scopes:
read: Read listings, reservations, conversations, page stats and plan status.
write: Create, edit, delete and publish listings; reply to buyers; share an address through the confirmed flow; set availability; create payment links.
bearerToken:
type: http
scheme: bearer
description: 'Firebase ID token for web UI authentication. A `Bearer cl_…` value is not a Firebase token: it is an API key or OAuth access token, covered by the apiKey and oauth2 schemes.'