Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tryhoard.com/llms.txt

Use this file to discover all available pages before exploring further.

Check pending

Check if the server wants Hoard to sync.
GET /api/v1/sync/pending

curl example

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://www.tryhoard.com/api/v1/sync/pending
Response:
{
  "action": "wait",
  "reason": "no_sync_needed",
  "sync_requested": false
}

Response fields

FieldTypeDescription
actionstring"sync" — run a cycle now. "wait" — poll again later.
reasonstringWhy the server made this decision. See values below.
sync_requestedbooleanWhether a sync was explicitly requested by the user from the dashboard.

reason values

ValueMeaning
manual_requestA sync is pending — either the user clicked “Sync Now” or the scheduler decided the interval has elapsed
no_sync_neededNo action needed, poll again
in_progressA sync is already running
auto_sync_pausedAuto-sync is disabled for this account
Clients do not need to distinguish manual from scheduled — the server is the single decision-maker. The original trigger is recorded server-side on sync_cycle_logs.trigger_source for analytics.

Agent heartbeat

Signal that Hoard is alive and check for updates.
POST /api/v1/heartbeat
Content-Type: application/json
Request body (optional):
{
  "version": "0.4.9",
  "platform": "darwin/arm64"
}
Response:
{
  "update_available": false,
  "force_update": false,
  "product_lines": [
    {"name": "magic", "category_id": 1},
    {"name": "pokemon", "category_id": 2}
  ]
}

Response fields

FieldTypeDescription
update_availablebooleanWhether a newer Hoard version is available.
force_updatebooleanWhether the local install must update before continuing (kill-switched version).
latest_versionstringLatest version string. Only present when update_available is true.
download_urlstringURL to download the new binary. Only present when update_available is true.
signaturestringEd25519 signature for verifying the download. Only present when update_available is true.
sha256stringSHA-256 checksum for verifying the download. Only present when update_available is true.
product_linesarrayProduct lines configured for this account. Each has name and category_id.

Upload inventory

Upload a gzipped inventory CSV from TCGplayer.
POST /api/v1/sync
Content-Type: application/gzip
X-Product-Line: magic
The request body is the gzip-compressed CSV data. The server queues it for processing, matches cards to the catalog, and calculates price updates.
HeaderRequiredDescription
Content-TypeYesMust be application/gzip or application/octet-stream
X-Product-LineNoWhich product line this upload belongs to (e.g., magic, pokemon, yugioh). Must match a configured product line.
Response (HTTP 202):
{"status": "accepted"}
Error responses:
CodeError codeMeaning
413payload_too_largeRequest body exceeds 10 MB on the wire
413decompressed_too_largeDecompressed gzip body exceeds 50 MB
422invalid_csvCSV could not be parsed
422invalid_gzipContent-Type is application/gzip but the body is not valid gzip
422invalid_product_lineX-Product-Line value not configured for account
422csv_processing_failedUnexpected error processing the CSV

Get price updates

Download a CSV of price changes to import back to TCGplayer.
GET /api/v1/export/price-updates
Returns a CSV string (text/csv), not JSON. Empty string if no updates are available. If a price rollback is pending, the rollback CSV is served instead.

Conditional GET (ETag)

When no rollback or Quick Add work is pending, the response includes a weak ETag fingerprint of the underpriced-cards set. Clients that send If-None-Match: <last-etag> will receive 304 Not Modified if nothing has changed since the last pull — avoiding the CSV serialization roundtrip on idle polls. Rollback and Quick Add responses always return 200 because they carry side-effectful state transitions that must run on every poll.

curl example

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://www.tryhoard.com/api/v1/export/price-updates \
  -o price-updates.csv

Request manual sync

Mark the account as having an explicit user-requested sync. The next poll on /api/v1/sync/pending returns action: "sync".
POST /api/v1/sync/request

curl example

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  https://www.tryhoard.com/api/v1/sync/request
Response:
{"status": "requested"}

Cooldown response (402)

When the per-account credits_metering_enabled flag is off (the default), this endpoint always returns 200 — behavior is unchanged from before the credits-metering feature shipped. When the flag is on AND the user is still within the post-sync cooldown window, the endpoint returns 402:
{
  "status": "cooldown",
  "cooldown_remaining_seconds": 878,
  "balance": 4,
  "top_up_path": "/billing/credit_packs"
}
The user can either wait the remaining seconds or call POST /api/v1/sync/skip_wait (below) to spend a credit and skip the wait.
FieldTypeDescription
statusstringAlways "cooldown" for this 402 branch.
cooldown_remaining_secondsintegerWhole seconds until the next manual sync is allowed.
balanceintegerThe user’s current credit balance.
top_up_pathstringRelative path to the top-up flow if the user wants to buy more credits.

Skip wait

Spend 1 credit to bypass the manual-sync cooldown. Wraps the Credits::SkipWaitCharge service which uses a FOR UPDATE row lock plus a reservation pattern so concurrent calls with the same idempotency key cannot double-charge.
POST /api/v1/sync/skip_wait
X-Idempotency-Key: <UUID>
This endpoint is independent of credits_metering_enabled — calling it IS the user’s explicit opt-in. A flag-OFF user with a balance can call it just fine; the gate is the balance, not the flag.

Headers

HeaderRequiredDescription
AuthorizationYesBearer YOUR_API_KEY
X-Idempotency-KeyYesCaller-supplied idempotency key (UUID recommended). Replaying the same key returns the original transaction without a second charge.

curl example

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479" \
  https://www.tryhoard.com/api/v1/sync/skip_wait
Response (200, charged):
{
  "status": "charged",
  "balance": 4,
  "cooldown_remaining_seconds": 852,
  "transaction_id": 18
}

Success statuses (HTTP 200)

StatusMeaning
chargedFirst call — 1 credit deducted, sync requested. Returns the new transaction_id.
replayedSame X-Idempotency-Key as a prior call. The original transaction_id is returned with no second charge.
no_charge_neededUser is already past cooldown — no charge taken.
sync_in_flightA sync is already running; from the caller’s POV the effect is achieved. Only status is returned in this branch.

Error responses

CodeStatusBodyMeaning
400{ "error": "X-Idempotency-Key header is required", "code": "idempotency_key_required" }The X-Idempotency-Key header is missing or blank.
402insufficient_balance{ "status": "insufficient_balance", "balance": 0, "top_up_path": "/billing/credit_packs" }User has 0 credits. Direct them to the top-up flow.
409mutex_held{ "status": "mutex_held", "retry_after_seconds": 30 }A sync, rollback, or Update Prices run is currently writing prices. Retry after retry_after_seconds.

Cancel sync

Release the sync lock if something goes wrong mid-sync.
POST /api/v1/sync/cancel
Response:
{"status": "cancelled"}
Returns 409 with code: "no_sync_in_progress" if no sync is running. The server auto-clears stale locks after 5 minutes, so this is a courtesy call.

Log sync cycle

Report the results of a completed sync cycle. Hoard sends this at the end of every cycle, whether it succeeded or failed.
POST /api/v1/sync/log
Content-Type: application/json
Request body:
{
  "status": "success",
  "sync_cycle_id": "20260506-abc123",
  "steps": [
    {"name": "login", "status": "success", "duration_ms": 1200},
    {"name": "export_inventory", "status": "success", "duration_ms": 8500},
    {"name": "upload_inventory", "status": "success", "duration_ms": 3200},
    {"name": "export_orders", "status": "failed", "error": "timeout waiting for download"}
  ],
  "agent_version": "0.4.9",
  "platform": "darwin/arm64",
  "duration_ms": 45000,
  "started_at": "2026-04-20T09:00:00Z"
}

Request fields

FieldTypeRequiredDescription
statusstringYesOverall result: "success", "partial", or "failed"
sync_cycle_idstringNoStable client-generated identifier for this sync cycle. Used to merge debug screenshots with the final step log.
stepsarrayNoEach step with a name, status, optional duration_ms, and optional error
agent_versionstringNoThe version of Hoard that ran this cycle
platformstringNoPlatform (e.g., darwin/arm64)
duration_msintegerNoTotal cycle duration in milliseconds
started_atstringNoISO 8601 start time. Defaults to server time if omitted.
Response (HTTP 201): Empty body.

Upload debug screenshot

Send a Base64-encoded screenshot to the server when a sync step fails.
POST /api/v1/sync/debug
Content-Type: application/json
The screenshot must be Base64-encoded PNG and must not exceed 2 MB decoded. Include sync_cycle_id so the server can attach the screenshot to the correct cycle even if the final sync log has not arrived yet. Without sync_cycle_id, at least one existing sync log entry must exist. Request body:
{
  "sync_cycle_id": "20260506-abc123",
  "step_name": "login",
  "page_url": "https://store.tcgplayer.com/admin",
  "screenshot": "<base64-encoded PNG>",
  "captured_at": "2026-04-20T09:01:30Z"
}
Response (HTTP 201): Empty body. If sync_cycle_id is present and the final sync log has not arrived yet, the server creates a placeholder failed log row and updates that row when POST /api/v1/sync/log arrives with the same identifier. Error responses:
CodeError codeMeaning
404No sync log entry exists yet
413payload_too_largeScreenshot exceeds 2 MB decoded