# API ## Base REST API under `/api/v1`. Responses use: ```json { "data": {} } ``` Collections include pagination metadata. ## Public endpoints ```text GET /api/v1/public/search GET /api/v1/public/products/:slug GET /api/v1/public/inventory GET /api/v1/public/inventory/:id GET /api/v1/public/listings GET /api/v1/public/listings/:id GET /api/v1/public/sellers GET /api/v1/public/lookups/categories GET /api/v1/public/lookups/conditions GET /api/v1/public/currencies GET /api/v1/public/exchange-rates ``` ## Authentication ```text POST /api/v1/auth/login POST /api/v1/auth/logout POST /api/v1/auth/forgot-password POST /api/v1/auth/reset-password GET /api/v1/auth/me POST /api/v1/auth/mfa/enroll POST /api/v1/auth/mfa/verify POST /api/v1/auth/mfa/disable GET /api/v1/auth/sessions DELETE /api/v1/auth/sessions/:id DELETE /api/v1/auth/sessions ``` `POST /api/v1/auth/forgot-password` always returns the same accepted response after validating an email-shaped request, whether or not an account exists. It sends an opaque, one-time reset token only through the configured mail adapter. `POST /api/v1/auth/reset-password` consumes that token, updates the password, and revokes all active sessions. `GET /api/v1/auth/sessions` returns only the authenticated user's active sessions and marks the current session. It never returns session or CSRF token material. `DELETE /api/v1/auth/sessions/:id` revokes one session owned by the authenticated user. `DELETE /api/v1/auth/sessions` revokes all other active sessions while retaining the current session. Session revocation endpoints require CSRF validation. ## Admin endpoints ```text GET /api/v1/admin/products POST /api/v1/admin/products GET /api/v1/admin/products/:id PATCH /api/v1/admin/products/:id GET /api/v1/admin/inventory POST /api/v1/admin/inventory GET /api/v1/admin/inventory/:id PATCH /api/v1/admin/inventory/:id POST /api/v1/admin/inventory/:id/list POST /api/v1/admin/inventory/:id/archive POST /api/v1/admin/inventory/:id/relist POST /api/v1/admin/inventory/:id/pricing/refresh GET /api/v1/admin/jobs/:id GET /api/v1/admin/products/:id/provider-mappings POST /api/v1/admin/products/:id/provider-mappings POST /api/v1/admin/provider-mappings/:id/disable GET /api/v1/admin/listings GET /api/v1/admin/listings/:id PATCH /api/v1/admin/listings/:id POST /api/v1/admin/listings/:id/publish POST /api/v1/admin/listings/:id/unpublish POST /api/v1/admin/listings/:id/pending POST /api/v1/admin/listings/:id/archive POST /api/v1/admin/pricing/refresh GET /api/v1/admin/jobs/:id POST /api/v1/admin/inventory/:id/images PATCH /api/v1/admin/inventory/:id/images/order POST /api/v1/admin/images/:id/primary DELETE /api/v1/admin/images/:id GET /api/v1/admin/acquisitions POST /api/v1/admin/acquisitions GET /api/v1/admin/sales POST /api/v1/admin/sales POST /api/v1/admin/sales/:id/void POST /api/v1/admin/pricing/observations POST /api/v1/admin/inventory/:id/valuation POST /api/v1/admin/inventory/:id/valuation/override POST /api/v1/admin/inventory/:id/pricing/refresh POST /api/v1/admin/bulk `POST /api/v1/admin/bulk` supports `ARCHIVE`, `PRICING_REFRESH`, and `TAGS` actions. `TAGS` accepts `add` and/or `remove` arrays and applies labels to the selected items' Products. GET /api/v1/admin/dashboard GET /api/v1/admin/exports/inventory.json GET /api/v1/admin/exports/inventory.csv GET /api/v1/admin/exports/inventory.xls POST /api/v1/admin/exports/inventory.json/jobs GET /api/v1/admin/exports/jobs/:id GET /api/v1/admin/exports/jobs/:id/download GET /api/v1/admin/settings PATCH /api/v1/admin/settings GET /api/v1/admin/saved-views POST /api/v1/admin/saved-views PATCH /api/v1/admin/saved-views/:id DELETE /api/v1/admin/saved-views/:id ``` Listing creation and lifecycle writes require an authenticated admin session and the session-bound CSRF header. Publishing is seller-scoped and checks the persisted product, category, condition, asking price, physical inventory, and image prerequisites before changing a draft to `AVAILABLE`; sold and archived physical inventory cannot be published. Unpublish changes an available listing to `CLOSED`; pending changes are available from draft or available listings. `POST /api/v1/admin/inventory/:id/images` stores validated image metadata for an owned physical inventory item. File bytes are handled through the `ImageStorage` adapter; JSON metadata and multipart image uploads are accepted. Multipart uploads derive dimensions from image bytes; supplied `width` and `height` fields are ignored. They validate the binary signature before storage. Publication counts persisted inventory images rather than trusting a listing request field. Acquisition, sale, pricing, and dashboard routes are seller-scoped through the authenticated admin session. All state-changing routes require the session-bound CSRF header. Sale creation marks owned active inventory items as `SOLD`; voiding retains the sale record, restores the items to `ACTIVE`, and records an audit event. Pricing observations remain private; public listing detail exposes only the latest calculated or manually overridden market value, condition label, and public image URLs. Pricing refresh returns `202 Accepted` with a durable job identifier and `PENDING` status. Poll the job endpoint for `PROCESSING`, `COMPLETED`, or `FAILED`; job status reads are seller-scoped and expose only safe status/error metadata. Refresh enqueue resolves the provider from an active Product-level mapping and returns `409` when no mapping exists. ## Provider matching The API must expose Product-level operations for: - searching configured providers for candidate matches - confirming a provider mapping - manually searching/refining a provider query - marking a provider not applicable - disabling/replacing a mapping Exact route naming may be finalized during API implementation, but these operations must remain Product-scoped. ## Validation Validate body, query, path, and response schemas. Business validation occurs after request-shape validation. Allowlist sortable fields. ## Errors Use machine-readable codes, including: - `PRODUCT_NOT_FOUND` - `INVENTORY_NOT_FOUND` - `INVENTORY_EDIT_LOCKED` - `INVALID_STATUS_TRANSITION` - `LISTING_REQUIREMENTS_NOT_MET` - `SALE_PRICE_REQUIRED` - `SALE_ITEM_NOT_AVAILABLE` - `PRICING_PROVIDER_FAILED` - `IMAGE_LIMIT_EXCEEDED` - `UNAUTHORIZED` - `FORBIDDEN` ## Privacy Public DTOs must never expose private acquisition, sale, buyer, internal, provider-debugging, pricing-history, or audit information. ## Pagination and query state Large collections use pagination. Public/admin filters and searches should be represented in URL query state where useful for shareability, refresh, and back/forward navigation. Public listing and product details include nullable `sellerDescription` from `general.publicDescription` in seller settings. Authenticated, seller-scoped `PATCH /api/v1/admin/settings` accepts this optional plain-text field (trimmed, maximum 2,000 characters); send an empty string to remove it. Omitted fields retain their current values. Private settings are not exposed in public DTOs. Seller contact removal uses the same explicit-empty-string convention: `general.publicContactEmail` accepts a valid email or `""`, and `general.publicContactPhone` accepts up to 50 characters or `""`. Both are trimmed; whitespace-only strings clear the contact. Omitted contact fields retain their saved values. `null` is not a settings input. Public listing/product detail DTOs return `null` for empty or absent contacts, so no contact link is rendered. `POST /api/v1/admin/images/:id/move` accepts exactly `{ "direction": "up" }` or `{ "direction": "down" }`. Authentication and session CSRF validation are required; ownership comes from the authenticated Seller. The server selects the adjacent image within the same physical Inventory Item and commits all resulting positions atomically. At the first/last position the corresponding move is a successful no-op. Missing/unowned images return `404 IMAGE_NOT_FOUND`; invalid bodies return `400 VALIDATION_ERROR`. The response uses the existing admin image DTO. The legacy absolute `/order` endpoint remains available, but UI moves use this new operation exclusively. Image update payloads are strict: - `PATCH /api/v1/admin/images/:id/order` requires exactly `sortOrder`, an integer from 0 through 2147483647 (the stored PostgreSQL integer range). - `PATCH /api/v1/admin/images/:id/alt-text` requires exactly `altText`, either trimmed text up to 1,000 characters or explicit `null`. An empty string clears the text; omission no longer implicitly clears it. Malformed, missing, mistyped, oversized, or extra fields return `400 VALIDATION_ERROR` without a persistence write. The service also enforces these constraints for non-HTTP callers. Existing ownership and CSRF checks apply. Multipart image uploads inspect and decode JPEG/PNG/WebP bytes before storage. Responses contain measured width/height and actual byte size. Invalid decoding returns `400 IMAGE_DECODE_INVALID`; existing file-size, signature and dimension limits remain enforced. The JSON metadata-registration path is unchanged.