← back to Govarbitrage
docs/API.md
156 lines
# GovArbitrage — HTTP API
All endpoints live under the Next.js App Router (`src/app/api`) and are marked
`dynamic = "force-dynamic"`. Responses are JSON. The base URL in local
development is `http://localhost:3000`.
The following endpoints exist today. Additional endpoints (listing **import**,
**buyer** interest/lead capture) may be added.
---
## `GET /api/listings`
Returns a paginated, filtered, sorted page of listings flattened into
`ListingRow` shape. Filtering, per-profile score selection, sorting, and
pagination are handled by `queryListings()` in `src/lib/listings.ts`.
### Query parameters
| Param | Type | Default | Notes |
| -------------------- | ----------------------------- | -------------------- | ------------------------------------------------------------------------------------- |
| `search` | string | — | Case-insensitive contains-match across `title`, `manufacturer`, `model`, `sourceAuctionId`. |
| `source` | `AuctionSource` enum | — | Exact match, e.g. `GOVDEALS`, `PUBLIC_SURPLUS`, `GSA_AUCTIONS`, `COUNTY`, `STATE_SURPLUS`, `UNIVERSITY_SURPLUS`, `MUNICIBID`, `BID4ASSETS`, `CSV`, `EXTENSION`, `OTHER`. |
| `category` | string | — | Case-insensitive contains-match on `category`. |
| `condition` | `Condition` enum | — | Exact match: `NEW`, `LIKE_NEW`, `USED_GOOD`, `USED_FAIR`, `FOR_PARTS`, `UNKNOWN`. |
| `risk` | `LOW` \| `MEDIUM` \| `HIGH` | — | Post-filter on the selected profile's risk rating. |
| `closingWithinHours` | number | — | Only listings whose `closingAt` is between now and now + N hours. |
| `sort` | `keyof ListingRow` | `opportunityScore` | Any `ListingRow` field (see response fields below). |
| `dir` | `asc` \| `desc` | `desc` | Sort direction. Numeric fields sort numerically; others by locale string compare. |
| `page` | number | `1` | 1-based page index. |
| `pageSize` | number | `50` | Rows per page. |
| `profile` | `ScoreProfile` enum | `OVERALL_OPPORTUNITY`| Which score profile drives the score columns + `risk`/`dropShip`. One of `OVERALL_OPPORTUNITY`, `BEST_ARBITRAGE`, `QUICK_FLIP`, `COLLECTOR`, `LOCAL_PICKUP`, `EASY_FREIGHT`, `PARTS_ONLY`, `HIGH_CONFIDENCE`, `HIGH_PROFIT`. |
### Response
```json
{
"rows": [ /* ListingRow[] */ ],
"total": 128,
"page": 1,
"pageSize": 50
}
```
- `total` is the count **after** filtering (including the risk post-filter), before pagination.
- `rows` is the current page slice.
### `ListingRow` fields
Each row is denormalized from `Listing + Research + CostBreakdown + chosen Score`
by `flattenListing()`. Money fields are numbers (USD); score fields are 0..100;
`roi` is a ratio (e.g. `0.42` = 42%). Fields typed `| null` are null when the
underlying research/cost/score is absent.
| Field | Type | Source |
| -------------------------------------- | --------------- | ---------------------------------------------------------------- |
| `id` | string | Listing id |
| `source` | string | `AuctionSource` |
| `sourceAuctionId` | string | Lot/auction number on the source site |
| `sourceUrl` | string \| null | |
| `title` | string | |
| `category` | string \| null | |
| `manufacturer` | string \| null | |
| `model` | string \| null | |
| `condition` | string | `Condition` |
| `quantity` | number | |
| `currentBid` | number | Live current bid |
| `currentCost` | number | Acquisition-in: winning bid + buyer premium + sales tax |
| `recommendedMaxBid` | number | Back-solved from target ROI (`CostBreakdown`) |
| `retailLow` | number \| null | `avgRetail` |
| `retailAverage` | number \| null | `avgRetail` |
| `retailHigh` | number \| null | `newRetail` |
| `usedLow` / `usedAverage` / `usedHigh` | number \| null | `usedLow` / `usedSoldPrice` / `usedHigh` |
| `wholesale` | number \| null | `wholesaleValue` |
| `liquidation` | number \| null | `liquidationValue` |
| `sellNow` | number \| null | `sellTodayValue` |
| `value7Day` / `value30Day` / `value90Day` | number \| null | Time-horizon values |
| `expectedSale` | number \| null | `expectedSalePrice` |
| `shipping` | number | Cost line item |
| `freight` | number | Cost line item |
| `repairs` | number | Cost line item |
| `marketplaceFees` | number | Cost line item |
| `netProfit` | number | `expectedNetProfit` |
| `roi` | number | Ratio |
| `risk` | string | Selected profile's risk (`LOW`/`MEDIUM`/`HIGH`) |
| `confidence` | number \| null | `confidenceScore` 0..100 |
| `opportunityScore` | number | Selected profile's `value` |
| `arbitrageScore` | number | Component sub-score |
| `demandScore` | number | Component sub-score |
| `velocityScore` | number | Component sub-score |
| `logisticsScore` | number | Component sub-score |
| `conditionScore` | number | Component sub-score |
| `competitionScore` | number | Component sub-score |
| `buyerScore` | number | Component sub-score |
| `dropShip` | string | Selected profile's drop-ship feasibility |
| `closingAt` | string \| null | ISO 8601 |
| `researchStatus` | string | `ResearchStatus` |
| `imageUrl` | string \| null | First image URL |
### Example
```bash
curl 'http://localhost:3000/api/listings?source=GOVDEALS&condition=USED_GOOD&profile=BEST_ARBITRAGE&sort=netProfit&dir=desc&closingWithinHours=48&page=1&pageSize=25'
```
---
## `GET /api/listings/[id]`
Returns a single full listing with all relations, or `404`.
### Path parameters
| Param | Type | Notes |
| ----- | ------ | ------------ |
| `id` | string | Listing id. |
### Response
The raw Prisma `Listing` record including these relations:
- `research` (1:1)
- `costBreakdown` (1:1)
- `scores` — ordered by `value` descending
- `comparables` — ordered by `price` descending
- `notes` — ordered by `createdAt` descending
- `events` — ordered by `createdAt` descending, most recent **20**
- `buyerLeads` — ordered by `createdAt` descending
- `buyerPage` (1:1)
- `outcome` (1:1)
On a missing id:
```json
{ "error": "Not found" }
```
with HTTP status `404`.
### Example
```bash
curl 'http://localhost:3000/api/listings/clx0abc123'
```
---
## Notes
- Both endpoints are read-only `GET`s and force dynamic rendering (no caching).
- `GET /api/listings` returns money as plain numbers (Prisma `Decimal` values are
converted to `number` in `flattenListing()`); `GET /api/listings/[id]` returns
the raw Prisma payload, where `Decimal` fields serialize as JSON strings.
- Import and buyer-workflow write endpoints are not yet implemented and may be
added.