Skip to main content

Overview

Base URL: https://api.exa.ai/monitors Auth: Pass your API key via the x-api-key header. Get one at https://dashboard.exa.ai/api-keys Monitors are scheduled, recurring Exa searches. You define a search query and a cron schedule, and the system runs the search automatically and delivers results to your webhook. Each run automatically deduplicates against previous results so you only see new content.

Installation

pip install exa-py    # Python
npm install exa-js    # JavaScript

Minimal Working Example

from exa_py import Exa
import os, time

exa = Exa(os.getenv("EXA_API_KEY"))

# 1. Create monitor
monitor = exa.monitors.create(params={
    "search": {
        "query": "AI startups that raised Series A funding",
        "numResults": 10
    },
    "webhook": {
        "url": "https://example.com/webhook"
    }
})

# Store the webhook secret for signature verification — only returned on creation
# See: Webhook Signature Verification section
print(monitor.webhook_secret)

# 2. Trigger a run and poll for results
exa.monitors.trigger(monitor.id)

while True:
    runs = exa.monitors.runs.list(monitor.id)
    latest = runs.data[0]
    if latest.status in ("completed", "failed"):
        break
    time.sleep(2)

# 3. Print results
if latest.status == "completed":
    run = exa.monitors.runs.get(monitor.id, latest.id)
    if run.output and run.output.results:
        for result in run.output.results:
            print(f"- {result['title']}: {result['url']}")

Endpoints

POST /monitors — Create a Monitor

Creates a monitor and returns it with a one-time webhookSecret. Request body:
FieldTypeRequiredDescription
namestringNoDisplay name for the monitor.
searchobjectYesSearch configuration. See Search Parameters.
triggerobjectNoCron schedule. See Trigger. Omit for manual-only monitors.
outputSchemaobjectNoJSON Schema for structured output. See Output Schema.
metadataobjectNoArbitrary key-value pairs for your own tracking.
webhookobjectYesWebhook configuration. See Webhook.
Response: A Monitor object with an additional webhookSecret field (string). Store this secret immediately — it is only returned once and is needed for webhook signature verification.

GET /monitors — List Monitors

Query params:
ParamTypeDefaultDescription
statusstringFilter by status: active, paused, or disabled.
cursorstringPagination cursor from a previous response’s nextCursor.
limitinteger50Results per page (1-100).
Response: { "data": [Monitor, ...], "hasMore": boolean, "nextCursor": string | null }

GET /monitors/{id} — Get a Monitor

Response: A Monitor object.

PATCH /monitors/{id} — Update a Monitor

All fields are optional. For search, you can send a partial object (only the fields you want to change). Set trigger to null to remove the schedule. Request body:
FieldTypeDescription
namestringUpdated name.
statusstringactive or paused.
searchobjectPartial search params to merge.
triggerobject or nullNew cron trigger, or null to remove.
outputSchemaobject or nullNew output schema, or null to remove. See Output Schema.
metadataobject or nullNew metadata, or null to remove.
webhookobjectPartial webhook params to merge.
Response: The updated Monitor object.

DELETE /monitors/{id} — Delete a Monitor

Response: The deleted Monitor object.

POST /monitors/{id}/trigger — Trigger a Run

Starts a run immediately, regardless of the cron schedule. Works for active and paused monitors. Response: { "triggered": true }

GET /monitors/{id}/runs — List Runs

Query params:
ParamTypeDefaultDescription
cursorstringPagination cursor.
limitinteger50Results per page (1-100).
Response: { "data": [Run, ...], "hasMore": boolean, "nextCursor": string | null }

GET /monitors/{id}/runs/{runId} — Get a Run

Response: A Run object.

Search Parameters

Nested under search in the create/update request.
ParameterTypeDefaultDescription
querystring(required)The search query to run. Supports natural language descriptions.
numResultsinteger10Number of results per run (1-100).
contentsobjectContent extraction options. See Contents Parameters.

Contents Parameters

Nested under search.contents. All fields are optional.
ParameterTypeDescription
textboolean or objectReturn full page text as markdown. Object form: { maxCharacters, includeHtmlTags, verbosity, includeSections, excludeSections }.
highlightsboolean or objectReturn key excerpts. Object form: { query, maxCharacters }.
summaryboolean or objectReturn LLM-generated summary. Object form: { query, maxTokens }.
extrasobjectExtract links and media: { links, imageLinks, richImageLinks, richLinks, codeBlocks } (all integers 0-1000).
contextboolean or objectReturn surrounding context. Object form: { maxCharacters }.
livecrawlstringCrawl strategy: "never", "always", "fallback", "auto", or "preferred".
livecrawlTimeoutintegerLivecrawl timeout in ms (0-90000).
maxAgeHoursintegerMax age of cached content in hours. 0 = always livecrawl. -1 = never livecrawl.
filterEmptyResultsbooleanFilter out results with no content.
subpagesintegerNumber of subpages to crawl per result (0-100).
subpageTargetstring or string[]Keywords to prioritize when selecting subpages.

Text Object Options

ParameterTypeDescription
maxCharactersintegerCharacter limit for returned text.
includeHtmlTagsbooleanPreserve HTML tags in output.
verbositystring"compact", "standard", or "full".
includeSectionsstring[]Only include these page sections: header, navigation, banner, body, sidebar, footer, metadata.
excludeSectionsstring[]Exclude these page sections. Same options as above.

Highlights Object Options

ParameterTypeDescription
querystringCustom query to direct highlight selection.
maxCharactersintegerMaximum characters for all highlights combined.

Summary Object Options

ParameterTypeDescription
querystringCustom query for the summary.
maxTokensintegerMaximum tokens for the summary.

Trigger

Nested under trigger in the create/update request.
FieldTypeRequiredDescription
typestringYesMust be "cron".
expressionstringYesStandard 5-field Unix cron expression. Minimum interval is 1 hour.
timezonestringNoIANA timezone identifier. Defaults to "Etc/UTC".
{
  "trigger": {
    "type": "cron",
    "expression": "0 9 * * 1",
    "timezone": "America/New_York"
  }
}

Webhook

Nested under webhook in the create/update request.
FieldTypeRequiredDescription
urlstringYesHTTPS URL. Must be a public endpoint (no localhost or private IPs).
eventsstring[]NoWhich events to deliver. Omit to receive all events.

Webhook Events

EventFired When
monitor.createdA new monitor is created
monitor.updatedA monitor is updated
monitor.deletedA monitor is deleted
monitor.run.createdA new run starts
monitor.run.completedA run finishes (success or failure)

Webhook Payload

{
  "id": "event_abc123",
  "object": "event",
  "type": "monitor.run.completed",
  "data": { },
  "createdAt": "2026-03-17T09:00:00Z"
}
The data field contains the full monitor or run object depending on event type.

Webhook Signature Verification

Webhook signature verification lets you confirm that incoming webhook requests actually came from Exa and haven’t been tampered with. Without verification, any external party that discovers your webhook URL could send fake payloads to your endpoint. Use the webhookSecret returned from the create endpoint to verify signatures on every incoming request. Every webhook delivery includes an Exa-Signature header:
Exa-Signature: t=1704729600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
To verify:
  1. Extract t (timestamp) and v1 (signature) from the header
  2. Construct the signed payload: {t}.{request_body}
  3. Compute HMAC-SHA256 of the signed payload using your webhook secret
  4. Compare the computed signature with v1 using constant-time comparison
import hmac
import hashlib

def verify_webhook(payload: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    timestamp = parts["t"]
    signature = parts["v1"]

    signed_payload = f"{timestamp}.{payload.decode()}"
    expected = hmac.new(
        secret.encode(), signed_payload.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Object Schemas

Monitor Object

{
  "id": "mon_abc123",
  "name": "Series A Tracker",
  "status": "active",
  "search": {
    "query": "AI startups that raised Series A funding",
    "numResults": 10,
    "contents": {
      "highlights": { "maxCharacters": 4000 }
    }
  },
  "trigger": {
    "type": "cron",
    "expression": "0 9 * * 1",
    "timezone": "America/New_York"
  },
  "outputSchema": null,
  "metadata": null,
  "webhook": {
    "url": "https://example.com/webhook",
    "events": ["monitor.run.completed"]
  },
  "nextRunAt": "2026-03-24T13:00:00.000Z",
  "createdAt": "2026-03-17T09:00:00.000Z",
  "updatedAt": "2026-03-17T09:00:00.000Z"
}
FieldTypeDescription
idstringUnique monitor identifier.
namestring or nullDisplay name.
statusstring"active", "paused", or "disabled". See Monitor Statuses.
searchobjectThe search configuration.
triggerobject or nullThe cron trigger, or null if manual-only.
outputSchemaobject or nullJSON Schema for structured output.
metadataobject or nullYour custom key-value pairs.
webhookobject{ url, events }.
nextRunAtstring or nullISO 8601 timestamp of the next scheduled run. null if no trigger.
createdAtstringISO 8601 creation timestamp.
updatedAtstringISO 8601 last-update timestamp.

Run Object

{
  "id": "run_xyz789",
  "monitorId": "mon_abc123",
  "status": "completed",
  "output": {
    "results": [
      {
        "title": "Acme AI raises $25M Series A",
        "url": "https://example.com/article",
        "publishedDate": "2026-03-10"
      }
    ],
    "content": "Structured output here (when outputSchema is set)",
    "grounding": [
      {
        "field": "content",
        "citations": [
          { "url": "https://example.com/article", "title": "Acme AI raises $25M Series A" }
        ],
        "confidence": "high"
      }
    ]
  },
  "failReason": null,
  "startedAt": "2026-03-17T09:00:01.000Z",
  "completedAt": "2026-03-17T09:00:45.000Z",
  "failedAt": null,
  "cancelledAt": null,
  "durationMs": 44000,
  "createdAt": "2026-03-17T09:00:00.000Z",
  "updatedAt": "2026-03-17T09:00:45.000Z"
}
FieldTypeDescription
idstringUnique run identifier.
monitorIdstringParent monitor ID.
statusstring"pending", "running", "completed", "failed", or "cancelled".
outputobject or nullSearch results and output. null until completed.
output.resultsarrayArray of search result objects (title, url, publishedDate, etc.).
output.contentanyStructured output when outputSchema is set.
output.groundingarrayField-level citations with confidence. See Grounding.
failReasonstring or nullWhy the run failed. See Fail Reasons.
startedAtstring or nullISO 8601 timestamp when execution began.
completedAtstring or nullISO 8601 timestamp when execution finished.
failedAtstring or nullISO 8601 timestamp if the run failed.
cancelledAtstring or nullISO 8601 timestamp if the run was cancelled.
durationMsinteger or nullTotal execution time in milliseconds.
createdAtstringISO 8601 creation timestamp.
updatedAtstringISO 8601 last-update timestamp.

Grounding

Each entry in output.grounding provides source citations for a field in the output:
FieldTypeDescription
fieldstringThe output field path (e.g. "content", "results[0].title").
citationsarraySources: { url, title }. Duplicate URLs are deduplicated.
confidencestring"low", "medium", or "high".

Monitor Statuses

Monitors have three possible statuses. An active monitor runs on its cron schedule and accepts manual triggers. A paused monitor stops running on schedule but still accepts manual triggers via the trigger endpoint — useful for temporarily halting a monitor without deleting it. A disabled monitor does not run at all; this status is set automatically by the system and cannot be set via the API.

Fail Reasons

ReasonDescriptionAction
api_key_invalidAPI key is invalid or revoked.Update your API key. Monitor auto-disables after 10 consecutive failures with this reason.
insufficient_creditsNot enough credits.Add credits to your account.
invalid_paramsSearch parameters are invalid.Fix the monitor’s search configuration.
rate_limitedToo many concurrent requests.Reduce monitor frequency or wait.
search_unavailableExa search backend is temporarily down.Retries on next scheduled run.
search_failedSearch execution failed.Check search parameters. Contact support if persistent.
internal_errorUnexpected error.Contact support if persistent.

Output Schema

outputSchema controls how the search synthesizes results into structured output. It supports two modes:

Text mode (default when no schema is provided)

When outputSchema is omitted or set to { "type": "text" }, the run output’s content field contains a plain text summary synthesized from the search results.
{
  "outputSchema": {
    "type": "text",
    "description": "A summary of recent AI funding rounds"
  }
}
The description field guides the synthesis. When outputSchema is omitted entirely, the system generates a text summary based on the search query.

Object mode

When type is "object", you provide a JSON Schema that defines the structure of the output. The search extracts and organizes information from results to match your schema.
{
  "outputSchema": {
    "type": "object",
    "description": "Structured competitor intelligence",
    "properties": {
      "headline": { "type": "string", "description": "One-line headline" },
      "category": {
        "type": "string",
        "enum": ["launch", "partnership", "hiring", "other"]
      },
      "summary": { "type": "string", "description": "2-3 sentence summary" }
    },
    "required": ["headline", "category", "summary"],
    "additionalProperties": false
  }
}
FieldTypeRequiredDescription
typestringYes"text" or "object".
descriptionstringNoGuides the synthesis. Useful for both modes.
propertiesobjectWhen type: "object"JSON Schema properties definition.
requiredstring[]NoWhich properties are required in the output.
additionalPropertiesbooleanNoWhether extra fields are allowed. Defaults to false.
When outputSchema is set, completed runs include:
  • output.content shaped to your schema
  • output.grounding with field-level citations and confidence scores

Automatic Deduplication

Monitors deduplicate results across runs using two layers: Date-based filtering. Each run only fetches content published or crawled since the last run. The system uses the cron schedule to compute a time window with a 2x overlap buffer, so content published between runs is captured even with slight timing variations. Semantic deduplication. The system tracks outputs from the last 5 runs and uses them to focus on new developments. This prevents the same stories or data points from appearing repeatedly.

Error Handling

HTTP StatusMeaning
400Bad request. Invalid parameters or invalid cron expression.
401Invalid or missing API key.
404Monitor or run not found.
422Validation error. Check parameter types and constraints.
429Rate limit exceeded.
Error response shape:
{
  "error": "Error message describing the issue"
}

Common Mistakes

LLMs frequently generate these incorrect patterns:
WrongCorrect
searchParams: { query: ... }Use search, not searchParams. The API field is search.
includeText / excludeText in search paramsThese fields do not exist on monitors. Use contents for content extraction options.
schedule: "0 * * * *"Use trigger: { type: "cron", expression: "0 * * * *" }. The schedule is nested under trigger.
Cron expressions with intervals under 1 hourMinimum interval is 1 hour. Expressions like */30 * * * * are rejected.
webhook: "https://..."webhook is an object: { url: "https://...", events: [...] }, not a plain string.
HTTP webhook URLsWebhook URLs must use HTTPS. HTTP URLs are rejected.
Localhost or private IP webhook URLsWebhook URLs must point to public endpoints.
Not storing webhookSecret on creationThe webhook signing secret is only returned once in the create response and is needed for signature verification. It cannot be retrieved later.

Patterns and Gotchas

  • Do not set a type field on search params. Monitors handle this internally. Runs typically take 5-60 seconds.
  • Store webhookSecret immediately. It is only returned in the create response and is needed for webhook signature verification. It cannot be retrieved later.
  • Use trigger for automation, manual trigger for testing. You can create a monitor without a trigger and use POST /monitors/{id}/trigger to run it on demand. This is useful for testing before adding a schedule.
  • Paused monitors still accept manual triggers. Set status to paused to stop the cron schedule while keeping the monitor available for on-demand runs.
  • outputSchema controls structured output. See Output Schema for details on type: "text" vs type: "object".
  • Python SDK response attributes use snake_case. Access response fields with snake_case: monitor.webhook_secret, monitor.next_run_at, run.fail_reason. Request dicts use camelCase keys matching the API (e.g., {"numResults": 10}). Alternatively, use typed Pydantic models (CreateSearchMonitorParams, UpdateSearchMonitorParams) with snake_case field names.
  • Webhook events default to all. If you omit events in the webhook config, all event types are delivered.
  • Overlap prevention. If a run is still in progress when the next scheduled time arrives, the in-progress run is cancelled.

SDK Auto-Pagination Helpers

Both SDKs provide helpers that handle pagination automatically when listing monitors or runs.
# Iterate through all monitors
for monitor in exa.monitors.list_all(status="active"):
    print(monitor.id)

# Collect all monitors into a list
all_monitors = exa.monitors.get_all(status="active")

# Iterate through all runs for a monitor
for run in exa.monitors.runs.list_all(monitor_id):
    print(run.id, run.status)

# Collect all runs into a list
all_runs = exa.monitors.runs.get_all(monitor_id)
SDKList (single page)Iterate all (auto-paginate)Collect all
Pythonexa.monitors.list()exa.monitors.list_all()exa.monitors.get_all()
Python (runs)exa.monitors.runs.list(id)exa.monitors.runs.list_all(id)exa.monitors.runs.get_all(id)
JavaScriptexa.monitors.list()exa.monitors.listAll()exa.monitors.getAll()
JavaScript (runs)exa.monitors.runs.list(id)exa.monitors.runs.listAll(id)exa.monitors.runs.getAll(id)

Complete Examples

Monitor with structured output and contents

{
  "name": "Competitor Tracker",
  "search": {
    "query": "Acme Corp product launches and partnerships",
    "numResults": 5,
    "contents": {
      "highlights": { "maxCharacters": 4000 },
      "text": { "maxCharacters": 10000 }
    }
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "headline": { "type": "string" },
      "category": { "type": "string", "enum": ["launch", "partnership", "hiring", "other"] },
      "summary": { "type": "string" }
    },
    "required": ["headline", "category", "summary"]
  },
  "trigger": {
    "type": "cron",
    "expression": "0 8 * * *",
    "timezone": "America/New_York"
  },
  "webhook": {
    "url": "https://example.com/webhook",
    "events": ["monitor.run.completed"]
  }
}

Manual-only monitor (no schedule)

{
  "name": "On-Demand Research",
  "search": {
    "query": "recent breakthroughs in quantum computing error correction",
    "numResults": 10
  },
  "webhook": {
    "url": "https://example.com/webhook"
  }
}

Full lifecycle

from exa_py import Exa
import os, time

exa = Exa(os.getenv("EXA_API_KEY"))

# 1. Create
monitor = exa.monitors.create(params={
    "name": "Funding Tracker",
    "search": {
        "query": "AI startups that raised Series A funding",
        "numResults": 10,
        "contents": {
            "highlights": {"maxCharacters": 4000}
        }
    },
    "trigger": {
        "type": "cron",
        "expression": "0 9 * * 1",
        "timezone": "America/New_York"
    },
    "webhook": {
        "url": "https://example.com/webhook",
        "events": ["monitor.run.completed"]
    }
})
print(f"Created: {monitor.id}")
print(f"Secret: {monitor.webhook_secret}")  # Store this!

# 2. Trigger a test run
exa.monitors.trigger(monitor.id)

# 3. Poll for completion
while True:
    runs = exa.monitors.runs.list(monitor.id)
    latest = runs.data[0]
    if latest.status in ("completed", "failed"):
        break
    time.sleep(2)

# 4. Fetch results
if latest.status == "completed":
    run = exa.monitors.runs.get(monitor.id, latest.id)
    if run.output and run.output.results:
        for result in run.output.results:
            print(f"- {result['title']}: {result['url']}")
else:
    print(f"Failed: {latest.fail_reason}")

# 5. Pause when not needed
exa.monitors.update(monitor.id, params={"status": "paused"})

# 6. Delete when done
exa.monitors.delete(monitor.id)