PoE2 sales feed — v1

A read-only history of your own Path of Exile 2 sales. Reachable at https://api.igrorik.com, or on the tailnet if you are on it.

Every sale the bot sees while polling your trade history is stored here before any notification goes out. The feed is a projection of that polling and never makes a request of its own to Path of Exile, so a sale appears here at poll cadence — roughly every 20 minutes — and not sooner.

Authentication

A bearer key, issued out of band and bound to one account. You only ever see your own sales; the query is scoped to your id server-side.

curl -H "Authorization: Bearer <key>" \
  "https://api.igrorik.com/v1/sales?limit=100"

Missing, malformed, revoked or wrong-scope keys all answer 401 alike.

Endpoints

MethodPathWhat
GET/v1/health{"ok": true}. No key needed.
GET/v1/salesYour sales, oldest-first, cursor-paged.
GET/v1/docsThis page.

Nothing else exists, and nothing here accepts anything but GET.

GET /v1/sales

QueryDefaultMeaning
afterthe start of retained history An opaque cursor from a previous response. Returns events strictly after it.
limit100 1–500. Anything else is 400.
{
  "events": [
    {
      "event_id": "poe2_9f1c…",
      "game": "poe2",
      "league": "Rise of the Abyssal",
      "item": {
        "name": "Mageblood",
        "base_type": "Heavy Belt",
        "text": null,
        "id": "5d1a…",
        "json": { "baseType": "Heavy Belt", "ilvl": 84, "explicitMods": ["…"], "…": "…" }
      },
      "price": { "amount": 120, "currency": "divine" },
      "sold_at": "2026-09-10T12:02:00Z",
      "detected_at": "2026-09-10T12:23:48Z",
      "detection_method": "trade_history"
    }
  ],
  "next_cursor": "djE6MTQyMA",
  "has_more": false,
  "retention": {
    "oldest_available_cursor": "djE6MA",
    "retained_since": "2026-09-15T21:18:33Z",
    "policy": "unlimited"
  }
}

Fields worth a sentence

FieldMeaning
event_id Stable identity, derived from source fields only — never from when we noticed it or how we classified it. Use it to deduplicate. The same item sold twice is two events.
item.json The trade API's own item object, verbatim: baseType, rarity, ilvl, implicitMods, explicitMods, runeMods, properties, requirements, sockets, icon, corrupted. Everything about the item lives here; item.name and item.base_type are lifted out for convenience.
item.text Always null. PoE's history endpoint carries no Ctrl-C item text.
price As listed, never converted. amount is a number or null, currency is the source string (divine, exalted, …).
sold_at The sale's own time from the source, ISO-8601 UTC, or null when it could not be parsed. Never a substitute for detected_at.
detected_at When the poll that found it ran. Every event from one poll shares it.
detection_method trade_history — the record was new when first seen. trade_history_backfill — it was already in the history window at that point. A first sync is mostly backfill: alert on trade_history only, or you will announce a year of sales at once. Both values mean a confirmed sale from PoE's own endpoint; nothing here is inferred from a listing disappearing.
There is no rendered message in an event. Compose your own line from item.name (or item.base_type) and price — the feed carries data, not someone else's formatting.

Paging

Cursors are opaque strings over an internal sequence, oldest-first. Keep the next_cursor from your last successful read and pass it as after next time; when has_more is true, keep going immediately instead of waiting. A cursor never expires on its own — only retention can outrun it.

Errors

Every error has the same shape: {"error": {"code": …, "message": …}}, sometimes with extra keys.

StatusCodeWhat to do
400invalid_limit, invalid_cursor Fix the request; a malformed cursor is never retried into working.
401unauthorizedThe key is missing, wrong or revoked.
410cursor_expired Your cursor predates retained history. The body carries oldest_available_cursor — resync from it rather than skipping silently.
429rate_limited 60 requests per minute per key. Retry-After and retry_after_seconds say how long.
500internalRetry later; nothing was lost.

Changes

WhenWhat
2026-09-18 notification_text removed. It carried the exact Telegram message, markdown and emoji included, which meant nothing to a client that is not Telegram. Compose your own line from item and price.
2026-09-18 detection_method means "was it new", not "was a message sent". Same two values; trade_history now appears on every sale newer than the last one seen, including ones the bot summarised rather than announced.