# Integrate with the auctionhouse.fyi API

Use the [developer docs](https://auctionhouse.fyi/docs/api) for the guided version. The contracts live in [api-v1](../protocol/api-v1.md), [strip-v1](../protocol/strip-v1.md) and [cdn-v1](../protocol/cdn-v1.md).

## One upload: an order book

A **scan** is an observation of the auction house. The **book** is the observed auction detail sent to `POST /api/v1/ah/books`. Each entry is `[key, price, stack, band, n]`: item variant, unit buyout in copper, units per auction, time-left band and count of identical auctions. The service derives price summaries, candles and depth from accepted books.

`POST /api/v1/ah/scans` is retired (404). Never construct a book from minimum prices and aggregate quantities: those summaries have already lost price levels, stack sizes, auction counts and time-left information. A producer with only browse summaries must wait for an actual book read. Existing read responses and historical contribution records may still use the word `scan`.

## Start with a read

```sh
curl --fail-with-body 'https://auctionhouse.fyi/api/v1/ah/forever.normal.alliance.us/14047'
```

No key is needed. The response's `row` is `[price, quantity, observationTime]` or null. HTTP times are Unix milliseconds, prices are integer copper. Public reads show the hourly snapshot (`live: false`); overall quantity listed is public. A null quantity in an older response means unknown, not zero supply. Volume estimates and per-level book depth require additional features.

## Authenticate and upload

1. Choose the API origin and obtain a credential **from that deployment**. API keys come from the account's API keys page and need `api.upload`; paired desktop device tokens use `upload.device`. Browser sessions cannot upload. Keep credentials outside Lua, source files and exported data.
2. Set `AHF_BASE` to the origin, without `/api/v1`, and `AHF_KEY` to the credential. `GET /api/v1/config` identifies the deployment; authenticated `GET /api/v1/me` lists features and limits.
3. Save your actual observations as `book.json`. The body is `{market, at, complete, auctions, entries}`. Entries must be unique and lexicographically ascending by `(key, price, stack, band)`, at most 200,000. `auctions` is the sum of `n`, not the sum of quantities.
4. With this repository's pinned Rust toolchain, run from `companion/`:

```sh
cargo run --example upload -- /path/to/book.json
```

The [uploader example](../companion/examples/upload.rs) reads the file, gzips it, sends it once through the companion client and prints the outcome. It preserves the timestamp, suggests retry delays, and exits unsuccessfully on quarantine or HTTP failure. It requires the repository's companion crate; it is not a standalone binary or screen-capture implementation.

Or use cURL with `AHF_BASE` and `AHF_KEY` already set:

```sh
gzip -c book.json > book.json.gz
curl --fail-with-body --show-error \
  -H "Authorization: Bearer $AHF_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Content-Encoding: gzip' \
  --data-binary @book.json.gz \
  "$AHF_BASE/api/v1/ah/books"
```

Plain JSON is accepted too. Both compressed and decompressed bodies must fit 32 MiB. `at` must be the original observation time: no more than 24 hours behind or five minutes ahead of the server clock. Never refresh a synthetic example's timestamp to upload it to production.

`price` is `ceil(totalStackBuyout / stack)`, or zero for bid-only auctions. `band` is 0 unknown, 1 short, 2 medium, 3 long, 4 very long. Preserve suffix and pet-species item keys. Only use `complete: true` after an unfiltered whole-market read finishes: it can mark absent keys no longer listed. Filtered and interrupted reads use false. Incomplete books preserve real auction detail; they are not summary uploads. Only complete books newer than the current book replace current depth; incomplete and late books are kept as historical snapshots and update eligible price summaries.

## Interpret the result

HTTP 200 returns `{at, credited, reason?}`. `at` is the market's newest merged observation, not an upload ID. `credited` is contribution credit, not a merge flag.

| Outcome | Action |
| --- | --- |
| `credited: true` | Merged and credited; done. |
| `incomplete`, `too_small`, `too_soon_credit` | Eligible readings merge without credit; done. |
| `disagrees` | Quarantined, not merged. Review market, variants, units and source data. |
| `duplicate` | This market and timestamp are already stored; done. |
| 400 `bad_book` / `bad_via` | Correct the payload, limits, timestamp or partner ID. |
| 401 / 403 | Correct the credential, deployment or feature access. |
| 409 `too_soon` | Wait five minutes; retry the same file. |
| 429 | Respect `Retry-After`, in seconds. |
| Network error / 5xx | Retry with exponential backoff and jitter, within the observation's 24-hour window. |

The new-book interval is five minutes per caller per market. Retry with the same account, credential, market and original `at`: recorded uploads return their first answer without merging twice. There is no `Idempotency-Key` header contract. Partner keys may send a validated `via` to limit per member; credit belongs to the partner account.

Credit requires a complete book with enough priced keys, agreement with recent other-account readings where enough exist to compare, and five minutes since the account's previous credited upload in that market. See the shared [CONTRIBUTION rules](../shared/src/entitlements.ts). Verify the result in contribution history. Public reads can lag until the hourly snapshot; a book-depth read requires `books`.

## Export from an addon

The working integration boundary is **addon → pixel strip → desktop screen capture → HTTP**. The external Rust client holds the token and selects the ruleset; faction comes from the strip. No direct in-game HTTP uploader or supported external addon callback exists.

The [Lua export helper](examples/export-book.lua) accepts the existing `ns.Strip` encoder and non-secret auction facts: `{id, suffix, pet, count, buyout, band}`. Here `buyout` is the **total stack buyout**, `pet` is the species ID, and `band` is the game's 0–3 value (or nil for unknown). The encoder normalizes prices and bands. Pass original Unix **seconds**, faction (1 Alliance, 2 Horde) and completeness.

It returns binary kind-3 bytes, grouped-entry count and auction count—not JSON or a file. Inside the existing transport, `ns.bulk:start(transferId, bytes)` queues painting; the scanner owns scheduling and replacement. See `Send` / `SendBulk` in [Auction.lua](../addon/AuctionHouseFYI/Auction.lua), [Strip.lua](../addon/AuctionHouseFYI/Core/Strip.lua), the [Rust decoder](../companion/src/strip/book.rs) and [upload pump](../companion/src/app/uploader.rs). Convert seconds to milliseconds once in the external client. Do not bypass the scheduler with competing transfers.

The addon only requests books. Missing replicate APIs, recent attempts or no answer leave it waiting, without a browse fallback. The desktop decoder still recognizes legacy kind-2 frames for diagnostics, but never queues or uploads them. Catalogue names can be learned passively from the player's browse results; they contain no market-price upload.

The [Forever API ledger](forever-api-ledger.md) records observed and unverified client behavior. Replicate reads and exact in-game pixel output remain unverified; Lua/decoder tests are not in-game evidence. SavedVariables cold-start loading is recorded as broken on the beta and is not the live export channel.

For changed contents of existing Lua files, run `/reload`. Adding addon folders or changing a TOC/file list needs a full restart. With the desktop app paired, open an auctioneer, run `/ahf status`, use `/ahf scan` if needed, and inspect status again, BugSack, the app's book queue and contribution history. No available book should mean waiting and no upload. Wowzers loaded means this addon stands down.
