Developer Docs

Auth & Error Handling: API Keys, Error Codes & Retry Strategies

Scraping is asynchronous: submit a run → poll → fetch data. See Quick Start for an end-to-end example; this page is the full reference for the envelope, statuses, billing, and errors.


Authentication

Every call carries an API Key in the request header:

Authorization: Bearer YOUR_API_KEY

Get Your API Key

  1. Log in to AntsData Console
  2. Go to API KeysCreate New Key
  3. Copy the key (format ants_xxxxxxxxxxxx; shown once — save it securely)

Security Best Practices

  • Don't hardcode or commit API Keys to Git; use environment variables; rotate regularly.

    export ANTSDATA_API_KEY="ants_xxxxxxxxxxxx" # Linux / macOS $env:ANTSDATA_API_KEY="ants_xxxxxxxxxxxx" # Windows PowerShell

    import os, requests API_KEY = os.getenv("ANTSDATA_API_KEY") H = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


Unified Response Envelope

All responses share one envelope; code === 0 means success:

{ "code": 0, "message": "ok", "data": <object> }
  • code == 0 is the only success test (no success boolean).
  • On error, the matching HTTP status code is returned with { "code": <int>, "message": <human-readable> }.
  • Billing is not on the top-level envelope but on the run object (chargedCredits / recordCount).

Run Lifecycle

POST /v1/scraper/{platform}/{resource}   → 201 { id, status:"queued" }         create run (async, holds credits)
                                          → 200 { status:"succeeded"|"failed" } sync, terminal within timeout
                                          → 202 { id, status:"queued"|"running"} sync timeout fallback (switch to polling)
GET  /v1/scraper/runs/{id}               → { status, targetsOk, tasks[], ... } poll until terminal
GET  /v1/scraper/runs/{id}/records?page=&pageSize=   → paginated records        fetch after success
GET  /v1/scraper/runs/{id}/download?format=json|csv|excel   → attachment download

The submit status code depends on deliveryMode: async is always 201; sync returns 200 when terminal, 202 on timeout fallback (see quick-start "Sync mode").

State Machine

queued ──▶ running ──▶ succeeded   (terminal)
                   └──▶ failed      (terminal)
  • Only 4 statuses: queued / running / succeeded / failed (no pending/completed/cancelled).
  • Terminal = succeeded | failed; stop polling once terminal.
  • No progress percentage: track progress via targetCount / targetsOk / targetsFailed.

Run Object Reference

POST (create) and GET /runs/{id} return the same Run object (data):

Field Type Description
id int64 Run number (job id; used by all follow-up queries)
actorName / endpointName string Platform / endpoint name
status string queued/running/succeeded/failed
deliveryMode string sync / async
recordLimit int Max records per target
targetCount int Total targets
targetsOk / targetsFailed int Succeeded / failed targets
recordCount int Total records produced
maxCost string Credits held at submission
chargedCredits string Actual charge ("0" before succeeded)
createdAt string ISO 8601
tasks[] array Per-target results (below)
deliveries[] array Per-delivery-channel status (when deliveries configured)

Per-Target Results tasks[]

A run's N targets fan out into N tasks; per-target success/failure lives here (not in the records):

Field Description
targetIndex Aligns with the request's targets[] index
taskId Platform-side task id (string)
submitState pending / submitted / submit_failed (dispatch state)
status queued / running / succeeded / failed (execution state)
recordCount Records produced for this target
errorMessage Failure reason (when failed)

Partial success: as long as ≥1 target succeeds, the run is succeeded and billed by successful records; failed targets each carry an errorMessage in tasks[].


Fetch & Download

  • Paginated records: GET /v1/scraper/runs/{id}/records?page=1&pageSize=20data = { total, page, pageSize, items:[...] }; pageSize default 20, max 100; empty until the run succeeds.
  • Download: GET /v1/scraper/runs/{id}/download?format=json|csv|excel (default json) → attachment stream.
  • Results are kept ~90 days — fetch them promptly.

Billing: Hold → Settle / Release

Billed by the number of successfully produced records (one unit price per 1,000 records):

Point Action Fields
Submit Hold maxCost = targetCount × ⌈recordLimit/1000⌉ × unit price; insufficient balance → submit rejected (402) maxCost
Success Settle chargedCredits = ⌈successful records / 1000 × unit price⌉ (≤ maxCost) chargedCredits / recordCount
Full failure Release the hold, no charge

Per-endpoint prices and plans are on the Pricing page.


Error Codes

Unified envelope errors: real HTTP status code + {code, message}.

Code Meaning Action
400 Invalid params / targets Check the body and target fields
401 Unauthorized / invalid API Key Check your key
402 Insufficient credits (submit rejected) Top up
404 Run not found / not yours Check the run id
429 Rate limited Slow down, see Retry-After
500 Server error Retry later
503 Service unavailable Retry later

Specific business code values follow the platform errcode (run domain 4000+, insufficient credits, etc.); final values are authoritative on the server.A single failed target does not produce a top-level error: the run can still be succeeded (partial success); failure detail is in tasks[].errorMessage.


Retry Strategy

import time

def poll_run(base, headers, run_id, max_wait=300):
    waited = 0
    for delay in [1, 2, 5, 5, 5, 5, 5, 5, 5, 5]:
        r = requests.get(f"{base}/scraper/runs/{run_id}", headers=headers)
        if r.status_code >= 500:            # server error: back off and retry
            time.sleep(delay); waited += delay; continue
        run = r.json()["data"]
        if run["status"] in ("succeeded", "failed"):
            return run
        time.sleep(delay); waited += delay
        if waited >= max_wait:
            raise TimeoutError(f"run {run_id} did not finish within {max_wait}s")
    return run
Situation Retry?
running while polling ✅ Back-off poll (1s→5s)
429 / 5xx ✅ Back off and retry (honor Retry-After)
401 / 400 / 402 ❌ Fix, then retry
run failed ❌ Inspect tasks[].errorMessage; resubmit failed targets if needed

Best Practices

  1. Store the API Key in an environment variable, never hardcode it.
  2. Poll with exponential backoff (1s→2s→5s cap); don't busy-poll; set a total timeout.
  3. One run, many targets: batching targets beats submitting one-by-one; per-target status is in tasks[].
  4. Set recordLimit sensibly to cap per-run cost; 0 (unlimited) computes the hold from available credits.
  5. Use /download for large results, /records for paged browsing.

Billing & Pricing

Billing is per successfully returned data record: each returned record is charged at that endpoint's unit price; failed records are not charged; the actual charge is reflected in the run's chargedCredits. Per-endpoint prices and plans are on the Pricing page.


Next Steps


FAQ

Q: Do API Keys expire?

No fixed expiry; revoke and regenerate anytime in the console. Use different keys for different apps/environments.

Q: Any recommended polling cadence?

Exponential backoff (1s→2s→5s cap); stop at succeeded/failed; avoid busy-polling that triggers rate limits.

Q: Am I charged for failed scrapes?

No. A fully-failed run releases the hold and isn't charged; partial success is billed only by the records produced.

Q: How do I view usage and billing?

Log in to the console Billing page for credit usage and invoices.

Q: How do I contact support?

Submit a ticket in the console, email [email protected], or join our Discord.