HyperbotsHyperAPI/Docs
Reference

Retrieving Results

Store the request_id, never the URLs. The text in a response is plain data — keep it as long as you like. The links are not: page images and figure crops arrive as signed storage URLs measured in minutes, not hours. The result behind them stays retrievable for 30 days from GET /v1/requests/{request_id}/snapshot, which signs a new set every time you call it.

Which parts expire

Two kinds of thing come back in a response. Values — extracted text, markdown, entities, line items, classifications, page counts, the request_id itself — never expire. They are yours to store and reuse indefinitely. Links — page images, advanced-parse figure crops, and the masked pages a /v1/redact call with output=urls returns — are signed storage URLs with a short life, and they are the only part of a response that goes stale.

/v1/classify, /v1/split and /v1/extract return no links at all — nothing in those responses can expire.

Why they are short-lived

A signed URL cannot outlive the credentials that signed it. The inference service signs with a temporary role session capped at one hour, so every link it issues necessarily expires well inside that hour. Asking for a longer window would not produce longer-lived links — it would produce links that silently die early.

This is not a plan limit. No tier extends it, and no endpoint is exempt. Rather than tracking which call yields which window, read assets_expire_at off the response.

Knowing when a payload goes stale

Responses that carry signed links also carry assets_expire_at — an ISO-8601 UTC timestamp naming the moment the links in that payload stop resolving. It is the earliest expiry across everything in the payload, so treating it as the deadline for the whole response is always safe. This is the authoritative answer: read it off the response rather than starting your own timer or assuming a fixed window.

{
  "status": "success",
  "request_id": "b91eaa9b-c75d-402d-a8dc-cb3af247c267",
  "task": "parse",
  "model_used": "HyperLM",
  "assets_expire_at": "2026-07-28T10:00:12+00:00",
  "result": {
    "ocr": "INVOICE\n\nAcme Corp ...",
    "pages": [
      {
        "page_number": 1,
        "text": "INVOICE\n\nAcme Corp ...",
        "image_url": "https://storage.example-cdn.com/deskewed/...&X-Amz-Signature=...",
        "dimensions": { "width": 1700, "height": 2200 }
      }
    ]
  },
  "duration_ms": 843,
  "metadata": {
    "pages": 1,
    "source": "document_key",
    "filename": "contract.pdf",
    "file_size": 5821
  }
}

If a response is going onto a queue, into a worker, or into a rendered page, compare assets_expire_at against the clock at the point of use. Past it, re-fetch rather than retry — an expired link returns an S3 error, and retrying it will never succeed.

Fetching fresh links

GET /v1/requests/{request_id}/snapshot returns the stored copy of a completed request and signs a new set of links on every call. There is no refresh endpoint for an individual URL — you re-read the request and take the links from the fresh response.

curl "https://apis.hyperbots.com/v1/requests/b91eaa9b-c75d-402d-a8dc-cb3af247c267/snapshot" \
  -H "X-API-Key: hk_live_your_key_here"
{
  "request_id": "b91eaa9b-c75d-402d-a8dc-cb3af247c267",
  "endpoint": "/v1/parse",
  "operation": "parse",
  "status": "completed",
  "filename": "contract.pdf",
  "page_count": 1,
  "created_at": "2026-07-28T09:15:12.481207+00:00",
  "expires_at": "2026-08-27T09:15:12.481207+00:00",
  "request_params": { "mode": "advanced" },
  "expired": false,
  "input": {
    "content_type": "application/pdf",
    "url": "https://storage.example-cdn.com/snapshots/...&X-Amz-Signature=..."
  },
  "result": {
    "status": "success",
    "request_id": "b91eaa9b-c75d-402d-a8dc-cb3af247c267",
    "task": "parse",
    "model_used": "HyperLM",
    "result": {
      "ocr": "INVOICE\n\nAcme Corp ...",
      "pages": [
        {
          "page_number": 1,
          "text": "INVOICE\n\nAcme Corp ...",
          "image_url": "https://storage.example-cdn.com/snapshots/...&X-Amz-Signature=..."
        }
      ]
    },
    "duration_ms": 843,
    "metadata": {
      "pages": 1,
      "source": "document_key",
      "filename": "contract.pdf",
      "file_size": 5821
    }
  }
}
FieldTypeDescription
expiredbooleanfalse when the stored input and result are still readable. true when the payload has passed its retention window — input and result are then null and the remaining fields are all you get back.
input.urlstring | nullSigned link to the exact bytes you originally uploaded. Freshly signed on this call. null if the link could not be signed.
input.content_typestringMIME type of the original upload, e.g. application/pdf.
resultobject | nullThe inference response exactly as the original call returned it, with every page image and figure crop re-signed for this call.
created_atstringISO-8601 UTC timestamp of the original request.
expires_atstringISO-8601 UTC timestamp when this request stops being retrievable — 30 days after created_at.
request_paramsobjectThe options the original call ran with, e.g. { "mode": "advanced" }.
endpoint / operation / status / filename / page_countstring / integerWhat was run, on what, and how it finished. Present whether or not the payload has expired.

The inference payload sits at result in the same shape the original call returned, so result.result.pages here is the same list as result.pages was there. Figure crops embedded in advanced-parse markdown and HTML are rewritten to fresh links too.

Requests are organization-scoped, exactly like jobs: any active API key in your organization can read any of your organization's requests, including ones started in the Playground. A request_id belonging to another organization returns the same 404 as one that was never stored.

Retention

Stored requests are kept for 30 days from the original call. The exact deadline for any request is on the response as expires_at. Once the stored payload is cleared, the request reports itself as expired rather than handing back links to files that no longer exist:

{
  "request_id": "3f0a1c58-9d21-4c7e-9a10-2b7b6c4e1d55",
  "endpoint": "/v1/parse",
  "operation": "parse",
  "status": "completed",
  "filename": "old-contract.pdf",
  "page_count": 12,
  "created_at": "2026-06-01T11:02:44.108331+00:00",
  "expires_at": "2026-07-01T11:02:44.108331+00:00",
  "request_params": { "mode": "fast" },
  "expired": true,
  "input": null,
  "result": null
}

A 404 means no stored copy exists for that request_id in your organization — it was never stored, it belongs to someone else, or it has been fully cleared.

What to store on your side

  • Store the request_id. It is stable, permanent, and the only handle you need to read a result back.
  • Don't persist signed URLs — not in a database row, not in a queued message, not in a rendered page or an email. They break within the hour.
  • Fetch on demand: read the request when you need to show it, and use the links from that response immediately.
  • Need the files beyond 30 days? Download the bytes while the links are live and keep them in your own storage.
# Store the request_id, not the URLs.
REQUEST_ID=$(curl -sS -X POST "https://apis.hyperbots.com/v1/parse?mode=advanced&include_image=true" \
  -H "X-API-Key: $API_KEY" \
  -F "document_key=$DOC_KEY" | jq -r .request_id)

# ... hours or days later, in a different process ...

# Re-fetch the result with links that work right now.
curl -sS "https://apis.hyperbots.com/v1/requests/$REQUEST_ID/snapshot" \
  -H "X-API-Key: $API_KEY" | jq -r '.result.result.pages[].image_url'

Already polling a job?

Polling GET /v1/jobs/{job_id} is how you collect a result, not how you keep its links alive. Take the links from a poll response and use them right away. If you need them again later — after the job has aged out, or because you never held the job_id — read the request back by request_id as above. See Async Mode.