Check pending
Check if the server wants Hoard to sync.curl example
Response fields
reason values
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.Response fields
Upload inventory
Upload a gzipped inventory CSV from TCGplayer.
Response (HTTP 202):
Upload inventory (V4 presigned-URL flow)
Newer agent builds upload the CSV directly to Cloudflare R2 using a short-lived presigned PUT URL, then notify Hoard with the resulting R2 key. Hoard’s bandwidth and Ruby heap never see the bytes. The flow is three calls:POST /api/v1/sync/upload-request— Hoard returns a 10-minute presigned PUT URL.PUT <upload_url>— agent uploads the raw CSV bytes directly to R2.POST /api/v1/syncwithContent-Type: application/jsonand body{key, product_line}— Hoard validates the key, enqueues the worker, and returns202.
users.use_r2_sync_pipeline = true; otherwise
they return 403 r2_pipeline_disabled and the agent should fall back to
the raw-CSV POST documented above.
Step 1: Mint a presigned URL
Error responses:
Step 2: PUT the CSV to R2
The PUT goes to Cloudflare, not Hoard — noAuthorization header
required (the presigned URL is the auth). Send the raw CSV bytes as the
PUT body.
Step 3: Notify Hoard with the key
Response (HTTP 202):
curl example (V4 round trip)
Get price updates
Download a CSV of price changes to import back to TCGplayer.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 weakETag 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
Request manual sync
Mark the account as having an explicit user-requested sync. The next poll on/api/v1/sync/pending returns action: "sync".
curl example
Cooldown response (402)
When the per-accountcredits_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:
POST /api/v1/sync/skip_wait (below) to spend a credit and skip the wait.
Skip wait
Spend 1 credit to bypass the manual-sync cooldown. Wraps theCredits::SkipWaitCharge service which uses a FOR UPDATE row lock plus a reservation pattern so concurrent calls with the same idempotency key cannot double-charge.
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
curl example
Success statuses (HTTP 200)
Error responses
Cancel sync
Release the sync lock if something goes wrong mid-sync.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.Request fields
Response (HTTP 201): Empty body.
Upload debug screenshot
Send a Base64-encoded screenshot to the server when a sync step fails.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 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: