ClearList Listings API

Create, read, update, delete, and bulk-create items.

Operations 8

GET /api/items List items #
POST /api/items Create a listing #
GET /api/items/{id} Get one item #
PUT /api/items/{id} Update a listing #
DELETE /api/items/{id} Soft-delete a listing #
POST /api/items/{id}/restore Restore a soft-deleted listing #
POST /api/items/bulk-upload Upload a photo batch (step 1 of bulk creation) #
POST /api/items/bulk-generate Generate one listing from photos (async) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • 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.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/clearlist-me:clearlist-me-listings-api"
All apis
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.

Get an API 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 Specification

clearlist-me-listings-api-openapi.yml Raw ↑
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.'