Files
2026-09-11 00:35:07 -04:00

9.1 KiB

API

Base

REST API under /api/v1.

Responses use:

{ "data": {} }

Collections include pagination metadata.

Public endpoints

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

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

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.