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.
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
| Field | Type | Description |
|---|
action | string | "sync" — run a cycle now. "wait" — poll again later. |
reason | string | Why the server made this decision. See values below. |
sync_requested | boolean | Whether a sync was explicitly requested by the user from the dashboard. |
reason values
| Value | Meaning |
|---|
manual_request | A sync is pending — either the user clicked “Sync Now” or the scheduler decided the interval has elapsed |
no_sync_needed | No action needed, poll again |
in_progress | A sync is already running |
auto_sync_paused | Auto-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
| Field | Type | Description |
|---|
update_available | boolean | Whether a newer Hoard version is available. |
force_update | boolean | Whether the local install must update before continuing (kill-switched version). |
latest_version | string | Latest version string. Only present when update_available is true. |
download_url | string | URL to download the new binary. Only present when update_available is true. |
signature | string | Ed25519 signature for verifying the download. Only present when update_available is true. |
sha256 | string | SHA-256 checksum for verifying the download. Only present when update_available is true. |
product_lines | array | Product 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.
| Header | Required | Description |
|---|
Content-Type | Yes | Must be application/gzip or application/octet-stream |
X-Product-Line | No | Which product line this upload belongs to (e.g., magic, pokemon, yugioh). Must match a configured product line. |
Response (HTTP 202):
Error responses:
| Code | Error code | Meaning |
|---|
| 413 | payload_too_large | Request body exceeds 10 MB on the wire |
| 413 | decompressed_too_large | Decompressed gzip body exceeds 50 MB |
| 422 | invalid_csv | CSV could not be parsed |
| 422 | invalid_gzip | Content-Type is application/gzip but the body is not valid gzip |
| 422 | invalid_product_line | X-Product-Line value not configured for account |
| 422 | csv_processing_failed | Unexpected 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:
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.
| Field | Type | Description |
|---|
status | string | Always "cooldown" for this 402 branch. |
cooldown_remaining_seconds | integer | Whole seconds until the next manual sync is allowed. |
balance | integer | The user’s current credit balance. |
top_up_path | string | Relative 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.
| Header | Required | Description |
|---|
Authorization | Yes | Bearer YOUR_API_KEY |
X-Idempotency-Key | Yes | Caller-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)
| Status | Meaning |
|---|
charged | First call — 1 credit deducted, sync requested. Returns the new transaction_id. |
replayed | Same X-Idempotency-Key as a prior call. The original transaction_id is returned with no second charge. |
no_charge_needed | User is already past cooldown — no charge taken. |
sync_in_flight | A sync is already running; from the caller’s POV the effect is achieved. Only status is returned in this branch. |
Error responses
| Code | Status | Body | Meaning |
|---|
| 400 | — | { "error": "X-Idempotency-Key header is required", "code": "idempotency_key_required" } | The X-Idempotency-Key header is missing or blank. |
| 402 | insufficient_balance | { "status": "insufficient_balance", "balance": 0, "top_up_path": "/billing/credit_packs" } | User has 0 credits. Direct them to the top-up flow. |
| 409 | mutex_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.
Response:
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
| Field | Type | Required | Description |
|---|
status | string | Yes | Overall result: "success", "partial", or "failed" |
sync_cycle_id | string | No | Stable client-generated identifier for this sync cycle. Used to merge debug screenshots with the final step log. |
steps | array | No | Each step with a name, status, optional duration_ms, and optional error |
agent_version | string | No | The version of Hoard that ran this cycle |
platform | string | No | Platform (e.g., darwin/arm64) |
duration_ms | integer | No | Total cycle duration in milliseconds |
started_at | string | No | ISO 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:
| Code | Error code | Meaning |
|---|
| 404 | — | No sync log entry exists yet |
| 413 | payload_too_large | Screenshot exceeds 2 MB decoded |