← 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.