ExonaExona API
Core Concepts

Scan Caching

Avoid redundant scans for the same company using the max_age_days parameter.

The problem

Risk profiles for a company rarely change significantly over a short period. Running a full scan every time you need to assess the same company wastes time, credits, and computing resources when the underlying data has not meaningfully changed.

The solution: max_age_days

When you create a scan, you can pass a max_age_days parameter. If a completed scan for the same company_website already exists and was completed within the last max_age_days days, Exona returns that existing scan immediately: no new analysis is run.

response = requests.post(
    f"{BASE_URL}/scans",
    headers=HEADERS,
    json={
        "company_name": "Acme AI Ltd",
        "company_website": "https://acme.ai",
        "max_age_days": 30,      # Accept a cached result up to 30 days old
    },
)
 
scan = response.json()
print(scan["cached"])           # True if returned from cache
print(scan["status"])           # "completed" (cache hit returns immediately)
print(scan["id"])               # The ID of the original scan

If a cache hit occurs, the response contains "cached": true alongside the original scan's id and completed_at. No credits are consumed.

If no recent scan exists, a new one is created as normal.


Choosing a max_age_days value

Use caseRecommended value
Renewals (annual)365: accept any scan from the current policy year
New business pipeline30: refresh monthly
High-velocity screening7: weekly freshness
Always freshOmit max_age_days or set to 0: always run a new scan

The right value depends on your workflow. For most renewal workflows, 30–90 days is a sensible default.


Forcing a refresh

If you want to run a new scan regardless of how recently the last one was completed: for example, because there has been a major public incident involving the company: pass "force_refresh": true.

response = requests.post(
    f"{BASE_URL}/scans",
    headers=HEADERS,
    json={
        "company_name": "Acme AI Ltd",
        "company_website": "https://acme.ai",
        "max_age_days": 30,
        "force_refresh": True,   # Override the cache: always run a new scan
    },
)

force_refresh takes precedence over max_age_days. A credit is consumed.


Cache matching

The cache is keyed on company_website (normalised: lowercase, stripped of trailing slashes). company_name is not part of the cache key: the same website with a slightly different name spelling will still hit the cache.

Only completed scans are cached. Failed scans are never returned as cache hits; a new scan will always be created if the last attempt failed.


Cache hit response

{
  "id": "scn_01hx...",
  "status": "completed",
  "cached": true,
  "cached_at": "2026-03-10T14:22:00Z",
  "created_at": "2026-03-10T14:20:00Z",
  "completed_at": "2026-03-10T14:21:45Z",
  "company": {
    "name": "Acme AI Ltd",
    "website": "https://acme.ai"
  },
  "result": { ... }
}

When "cached": true, the cached_at field shows when the cache hit occurred (now), and completed_at shows when the underlying scan was originally completed.


Questionnaire and caching

If you submit a questionnaire alongside max_age_days, the cache is bypassed. Questionnaire data is company-submission-specific, so a previously cached scan (which may have used different questionnaire answers, or none at all) is not considered a valid match. A new scan will always be created when questionnaire data is present.

To take advantage of caching in questionnaire workflows, store the resulting scan ID and retrieve it directly by ID on subsequent calls.

On this page