{
  "openapi": "3.1.0",
  "info": {
    "title": "ClearList API",
    "version": "0.3.0",
    "description": "API for the ClearList AI-powered moving sale platform. Agents authenticate via an API key acquired through the email verification flow (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: write endpoints that create records accept an Idempotency-Key header (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. Error responses are never stored, so a corrected request always re-executes.",
    "contact": {
      "email": "hello@clearlist.me",
      "url": "https://clearlist.me"
    }
  },
  "servers": [
    {
      "url": "https://clearlist.me",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Onboarding",
      "description": "Email-OTP auth and API-key acquisition."
    },
    {
      "name": "Listings",
      "description": "Create, read, update, delete, and bulk-create items."
    },
    {
      "name": "Publishing",
      "description": "Publish the shareable sale page."
    },
    {
      "name": "Orders",
      "description": "Reservations and buyer conversations."
    },
    {
      "name": "Scheduling",
      "description": "Pickup availability."
    },
    {
      "name": "Account",
      "description": "Tier, capacity, and payment links."
    },
    {
      "name": "Jobs",
      "description": "Async job polling."
    },
    {
      "name": "System",
      "description": "Health and status probes."
    }
  ],
  "components": {
    "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 }`."
      },
      "bearerToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Firebase ID token for web UI authentication."
      }
    },
    "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."
          }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "data": {
            "type": "object",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          }
        }
      }
    },
    "parameters": {
      "CursorParam": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Pagination cursor: pass the `next_cursor` (ISO timestamp) from a previous response to fetch the next page."
      },
      "LimitParam": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 50,
          "maximum": 100,
          "minimum": 1
        },
        "description": "Maximum number of records to return (max 100)."
      },
      "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). The first successful (2xx) response is stored for 24h; retrying with the same key replays it instead of re-executing, so network-failure retries never create duplicates. A concurrent duplicate gets 409 idempotency_conflict. Error responses (4xx/5xx) are never stored — a corrected request re-executes.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[A-Za-z0-9_-:.]+$"
        }
      }
    },
    "headers": {
      "WWWAuthenticate": {
        "schema": {
          "type": "string"
        },
        "description": "RFC 9728 challenge: `Bearer resource_metadata=\"https://clearlist.me/.well-known/oauth-protected-resource\", error=\"invalid_token\"`."
      },
      "RateLimitLimit": {
        "schema": {
          "type": "integer"
        },
        "description": "Request quota for the current window."
      },
      "RateLimitRemaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests remaining in the current window."
      },
      "RateLimitReset": {
        "schema": {
          "type": "integer"
        },
        "description": "Unix epoch seconds when the window resets."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid or missing parameters.",
        "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"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/auth/send-code": {
      "post": {
        "operationId": "sendVerificationCode",
        "x-tool-id": "send_verification_code",
        "tags": [
          "Onboarding"
        ],
        "summary": "Send verification code",
        "description": "Sends a 6-digit verification code to the given email address. No authentication required.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/auth/verify-code": {
      "post": {
        "operationId": "verifyCode",
        "x-tool-id": "verify_code",
        "tags": [
          "Onboarding"
        ],
        "summary": "Verify code and get API key",
        "description": "Verifies the 6-digit code. Creates the account if new. Returns an API key when `agent: true` is set.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "code"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string",
                    "pattern": "^[0-9]{6}$"
                  },
                  "agent": {
                    "type": "boolean",
                    "description": "Set true to receive an API key."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "isNewUser": {
                      "type": "boolean"
                    },
                    "uid": {
                      "type": "string"
                    },
                    "apiKey": {
                      "type": "string",
                      "description": "Only when agent=true. Format cl_<64-hex>."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/items": {
      "get": {
        "operationId": "getListings",
        "x-tool-id": "get_listings",
        "tags": [
          "Listings"
        ],
        "summary": "List items",
        "description": "Returns all items for the authenticated seller.",
        "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"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createListing",
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Item limit reached for current tier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "operationId": "editListing",
        "x-tool-id": "edit_listing",
        "tags": [
          "Listings"
        ],
        "summary": "Update a listing",
        "description": "Update any field on an item (price, title, description, status, etc.).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "deleteListing",
        "x-tool-id": "delete_listing",
        "tags": [
          "Listings"
        ],
        "summary": "Delete a listing",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/items/bulk-upload": {
      "post": {
        "operationId": "bulkCreateListings",
        "x-tool-id": "bulk_create_listings",
        "tags": [
          "Listings"
        ],
        "summary": "Bulk-create listings (async)",
        "description": "Accepts up to 50 photos. AI groups them into items and generates listings. Returns a job_id immediately (HTTP 202); poll GET /api/ai-jobs/{jobId} for the result. 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": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "job_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/api/ai-jobs/{jobId}": {
      "parameters": [
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getJobStatus",
        "tags": [
          "Jobs"
        ],
        "summary": "Poll an async job",
        "description": "Returns the status of an async AI job. Poll with exponential backoff (2s → 10s cap); jobs expire after 15 minutes.",
        "responses": {
          "200": {
            "description": "Job status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/pages/publish": {
      "post": {
        "operationId": "publishPage",
        "x-tool-id": "publish_page",
        "tags": [
          "Publishing"
        ],
        "summary": "Publish sale page",
        "description": "Publishes the seller's sale page with location info and returns the shareable URL. Naturally idempotent: re-publishing with the same body converges to the same page state, so retries are safe without an Idempotency-Key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "city": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-idempotent": true
      }
    },
    "/api/conversations": {
      "get": {
        "operationId": "getReservations",
        "x-tool-id": "get_reservations",
        "tags": [
          "Orders"
        ],
        "summary": "List conversations / reservations",
        "description": "Returns buyer conversations and reservations for the seller, newest first. Cursor-paginated; the response includes `next_cursor` when more pages exist.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CursorParam"
          },
          {
            "$ref": "#/components/parameters/LimitParam"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "conversations": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "next_cursor": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/conversations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "replyToBuyer",
        "x-tool-id": "reply_to_buyer",
        "tags": [
          "Orders"
        ],
        "summary": "Reply to a buyer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/scheduling/availability": {
      "put": {
        "operationId": "setAvailability",
        "x-tool-id": "set_availability",
        "tags": [
          "Scheduling"
        ],
        "summary": "Set pickup availability",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/payments/status": {
      "get": {
        "operationId": "checkTierStatus",
        "x-tool-id": "check_tier_status",
        "tags": [
          "Account"
        ],
        "summary": "Check tier, capacity, and expiry",
        "responses": {
          "200": {
            "description": "Tier status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "tier": {
                          "type": "string"
                        },
                        "items_remaining": {
                          "type": "integer"
                        },
                        "expires_at": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/payments/checkout-link": {
      "post": {
        "operationId": "generatePaymentLink",
        "x-tool-id": "generate_payment_link",
        "tags": [
          "Account"
        ],
        "summary": "Generate a checkout link",
        "description": "Returns a Stripe checkout URL for the requested plan. Send the URL to the user to pay in a browser; poll /api/payments/status to confirm. Naturally idempotent: no record is created; retries return the same payment link.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan"
                ],
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "sale_pass",
                      "big_move"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-idempotent": true,
        "x-payment-info": {
          "intent": "charge",
          "method": "stripe",
          "amount": 20,
          "currency": "USD",
          "description": "One-time pass purchase via Stripe Checkout (no subscription). Amount depends on the plan in the request body: sale_pass = 20.00 USD, big_move = 39.00 USD. The response returns a Stripe-hosted checkout URL; a human completes payment in the browser and the account upgrades via webhook."
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "System"
        ],
        "summary": "Liveness probe",
        "description": "Zero-dependency liveness check. Always 200 if the process is alive.",
        "security": [],
        "responses": {
          "200": {
            "description": "Alive.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/status": {
      "get": {
        "operationId": "getStatus",
        "tags": [
          "System"
        ],
        "summary": "Dependency-aware status",
        "description": "Checks Firestore and Stripe connectivity. Returns 'ok' or 'degraded'.",
        "security": [],
        "responses": {
          "200": {
            "description": "Status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ]
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "version": {
                      "type": "string"
                    },
                    "checks": {
                      "type": "object",
                      "properties": {
                        "firestore": {
                          "type": "boolean"
                        },
                        "stripe": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
