API Reference

API Reference

Base URL: https://api.verid.dev

Version: 2026-05-01

All requests must include Authorization: Bearer YOUR_API_KEY where YOUR_API_KEY starts with vrd_. All request and response bodies are JSON.

Pagination is uniform across list endpoints: pass page (default 1, min 1) and limit (default 20, min 1, max 100) as query parameters. List responses always return:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 42,
    "totalPages": 3,
    "hasMore": true
  }
}

Monitors

List monitors

GET /v1/monitors

Query paramTypeDefaultConstraints
pageinteger1>= 1
limitinteger201100
visualboolean-true = only monitors with the visual layer enabled; false = only without

Create a monitor

POST /v1/monitors

FieldTypeRequiredDefaultConstraints / Allowed values
namestringyes-1–200 characters
urlstringyes-Must be a valid URL
schedule_interval_secondsintegeryes-Positive integer; tier-gated minimum (free: 86400, starter: 3600, pro: 900, scale: 300)
extract_configobjectyes-See Extract config
diff_predicateobjectyes-See Diff predicate
deliveriesarrayno[]0–25 items; tier-gated max per monitor (free: 1, starter: 3, pro: 10, scale: 25). See Delivery config
template_slugstringnonullMarks the monitor as created from a template
fetch_modestringno"auto""auto" | "browser"
request_headersobjectnonullSee Request headers
visual_configobjectnonullSee Visual config; tier-gated (visualMonitors)

Returns 201 Created with the full monitor object.

Extract config

Discriminated by method. One of:

methodExtra fields
"css"fields: { [name]: cssSelector }
"xpath"fields: { [name]: xpathExpression }
"json_path"fields: { [name]: jsonPathExpression }
"regex"fields: { [name]: regexPattern }
"full_page"(no extra fields - hashes the rendered document)
"prompt"prompt: string (10–2000 chars), optional schema: object (JSON-Schema-shaped hints)

See Extraction methods for worked examples.

Diff predicate

Discriminated by type. One of:

typeRequired fields
"any_field_changes"-
"field_changes"field: string
"field_increases_by_percent"field: string, threshold: number (positive)
"field_decreases_by_percent"field: string, threshold: number (positive)
"field_increases_by_absolute"field: string, threshold: number
"field_decreases_by_absolute"field: string, threshold: number
"field_matches_regex"field: string, pattern: string
"field_equals"field: string, value: string | number | boolean
"composite"operator: "AND" | "OR", conditions: DiffPredicate[] (min 1)

See Predicates for examples and semantics.

Delivery config

Discriminated by type. Each item in deliveries must be one of:

typeRequired fields
"webhook"url: string (must be URL), optional headers: { [name]: value } (subject to Request headers rules)
"slack"webhookUrl: string (Slack incoming webhook URL)
"discord"webhookUrl: string (Discord webhook URL)
"email"to: string (email address)

Request headers

request_headers (on the monitor, applied to outgoing scrape requests) and headers (on a webhook delivery, applied to outbound webhook calls) share the same validation:

  • At most 20 headers.
  • Header name: 1–256 chars, RFC 7230 token chars only ([!#$%&'*+\-.^_|~0-9a-zA-Z]+`).
  • Header value: max 2048 chars, printable ASCII + tab (no CR/LF).
  • Reserved names rejected: host, content-length, connection, transfer-encoding, cookie.

Visual config

Additive screenshot layer, independent of the extraction method: when enabled, every check also screenshots the page (or the picked regions) and pixel-diffs it against the previous run. See Visual monitoring for the full guide.

FieldTypeRequiredDefaultConstraints
enabledbooleanyes--
thresholdPctnumberno10100; deliveries fire when the changed-pixel area reaches this percent
fullPagebooleannotrueFull scroll height vs viewport only
regionsarrayno[]Max 20. Each region has exactly one of selector (CSS, re-resolved every run) or rect ({x, y, width, height} in full-page CSS pixels), plus an optional label
ignoreSelectorsarrayno-Max 20 CSS selectors hidden before the shot (cookie banners, ads, tickers)
viewportobjectno1280×720{width: 320–3840, height: 240–2160}

Get a monitor

GET /v1/monitors/:id

Returns the monitor object:

{
  "id": "uuid",
  "user_id": "uuid",
  "name": "string",
  "url": "string",
  "schedule_interval_seconds": 3600,
  "extract_config": { "...": "..." },
  "diff_predicate": { "...": "..." },
  "deliveries": [],
  "status": "active | paused | billing_paused | error | deleted",
  "template_slug": "string | null",
  "consecutive_failures": 0,
  "fetch_mode": "auto | browser",
  "request_headers": { "...": "..." },
  "visual_config": { "...": "..." },
  "last_run_at": "ISO 8601 | null",
  "next_run_at": "ISO 8601 | null",
  "created_at": "ISO 8601"
}

Update a monitor

PATCH /v1/monitors/:id

All create fields are patchable except template_slug (set once on creation). Same constraints and tier gates apply.

Delete a monitor

DELETE /v1/monitors/:id

Returns 204 No Content.

Pause / Resume a monitor

POST /v1/monitors/:id/pause - sets status: "paused".

POST /v1/monitors/:id/resume - sets status: "active" and reschedules an immediate run. Returns 403 FORBIDDEN if the monitor is billing_paused (locked because the user is over their plan's monitor cap).

Both take an empty JSON body ({}).

Trigger a manual run

POST /v1/monitors/:id/run

Empty body. Counts against the tier's runNowPerDay quota (free: 5, starter: 50, pro: 500, scale: 5000).

Returns { "run_id": "uuid", "queued": true }.

List runs for a monitor

GET /v1/monitors/:id/runs

Supports page and limit (same defaults as the global list).

Visual screenshot history

GET /v1/monitors/:id/visual-history?limit=20

Newest-first screenshot history for a visual monitor (limit: 1–20, default 20). Thumbnail URLs are signed and short-lived — request them when needed rather than storing.

{
  "data": [
    {
      "run_id": "uuid",
      "started_at": "ISO 8601",
      "change_pct": 6.1,
      "has_changes": true,
      "thumb_url": "https://...signed..."
    }
  ]
}

Runs

Get a run

GET /v1/runs/:id

{
  "id": "uuid",
  "monitor_id": "uuid",
  "user_id": "uuid",
  "started_at": "ISO 8601",
  "completed_at": "ISO 8601 | null",
  "status": "running | success | error | skipped",
  "extracted": { "...": "..." },
  "diff": { "...": "..." },
  "delivery_triggered": true,
  "fetch_method": "static | browser | proxy | null",
  "duration_ms": 1234,
  "error_message": "string | null",
  "visual_change_pct": 6.1
}

visual_change_pct is only set on runs of monitors with the visual layer enabled.

List deliveries for a run

GET /v1/runs/:id/deliveries

Returns { "data": [Delivery, ...] } (not paginated - one run produces at most maxDeliveriesPerMonitor rows).

Replay all failed deliveries for a run

POST /v1/runs/:id/replay

Re-queues every failed or dead delivery on the run. Returns { "queued": <number> }.

Visual run images

GET /v1/run-images/:id

Signed before/after/diff-overlay image URLs for a visual run, per region. URLs are short-lived — request them when needed rather than storing. Returns 404 if the run has no visual capture. beforeUrl comes from the previous screenshot-bearing run (null on the first capture); diffUrl is the red-highlight pixel-diff overlay (null when nothing changed).

{
  "kind": "visual",
  "aggregateChangePct": 6.1,
  "hasChanges": true,
  "regions": [
    {
      "regionIndex": 0,
      "label": "Pricing table",
      "changedPct": 6.1,
      "hasChanges": true,
      "beforeUrl": "https://...signed... | null",
      "afterUrl": "https://...signed...",
      "diffUrl": "https://...signed... | null"
    }
  ]
}

Deliveries

List deliveries

GET /v1/deliveries

Query paramTypeDefaultAllowed values
pageinteger1>= 1
limitinteger201100
statusstringall"pending" | "success" | "failed" | "dead"

Results are restricted to the tier's retention window (free: 14 days, starter: 180, pro: 365, scale: 730). The response also returns the retention envelope:

{
  "data": [...],
  "pagination": { "...": "..." },
  "retention": {
    "tier": "free",
    "retention_days": 14,
    "cutoff_at": "ISO 8601"
  }
}

Get a delivery

GET /v1/deliveries/:id

{
  "id": "uuid",
  "monitor_id": "uuid",
  "user_id": "uuid",
  "run_id": "uuid",
  "delivery_type": "webhook | slack | discord | email",
  "destination": "string",
  "payload": { "...": "..." },
  "status": "pending | success | failed | dead",
  "attempts": 0,
  "last_attempt_at": "ISO 8601 | null",
  "next_attempt_at": "ISO 8601 | null",
  "response_status": 200,
  "response_body": "string | null",
  "created_at": "ISO 8601"
}

Replay a delivery

POST /v1/deliveries/:id/replay

Empty body. Resets the delivery to pending and re-queues it immediately. Returns { "queued": true }.

Templates

List templates

GET /v1/templates

Query paramTypeAllowed values
categorystringFilter to one category. Common values: developer, crypto, ecommerce, news, status, generic

Each item:

{
  "slug": "string",
  "name": "string",
  "description": "string",
  "category": "string",
  "audience": "developer | operator | both",
  "example_url": "string | null",
  "config": {
    "extract": { "...": "..." },
    "diff_predicate": { "...": "..." },
    "schedule_interval_seconds": 3600
  },
  "created_at": "ISO 8601"
}

Create monitor from template

POST /v1/monitors/from-template/:slug

FieldTypeRequiredNotes
namestringnoOverrides the template's default name
urlstringnoOverrides the template's example_url
deliveriesarraynoSame shape as Delivery config

The template's schedule_interval_seconds is clamped up to the caller's tier minimum if it would otherwise violate it.

Available template slugs: aws-status-page, crypto-price-coingecko, generic-css-selector, generic-full-page, generic-json-field, generic-llm-prompt, github-new-release, hacker-news-front-page, job-listings-css, npm-new-version, pypi-new-version, rss-new-item, shopify-product-price, sitemap-new-url, stock-price-yahoo.

API Keys

List API keys

GET /v1/keys

Returns { "data": [ApiKey, ...] }. The key_hash column is never returned - only key_prefix (the first 8 chars, safe to display).

Create API key

POST /v1/keys

FieldTypeRequiredDefaultConstraints / Allowed values
namestringyes-1–100 characters
scopesstring[]no["read"]Each scope is a string. Recognized: "read", "write"
expires_atstringno(no expiry)ISO 8601 datetime

Returns 201 Created:

{
  "key": "vrd_...",         // shown exactly once
  "apiKey": { "id": "...", "name": "...", "scopes": [...], "expires_at": "..." }
}

Rotate API key

POST /v1/keys/:id/rotate

FieldTypeRequiredConstraints
grace_period_hoursintegeryes0168 (0 = revoke old immediately; otherwise old key keeps working for N hours)

Returns the same shape as create. The new key inherits the old key's remaining expiry window.

Delete API key

DELETE /v1/keys/:id

Returns 204 No Content.

Usage

Get usage summary

GET /v1/usage

{
  "monitorsActive": 3,
  "llmCallsThisMonth": 12,
  "runNowToday": 1,
  "tier": "free | starter | pro | scale",
  "limits": {
    "maxMonitors": 5,
    "minIntervalSecs": 86400,
    "retentionDays": 14,
    "maxDeliveriesPerMonitor": 1,
    "llmCallsPerMonth": 50,
    "runNowPerDay": 5,
    "proxyBytesPerMonth": 0
  }
}

Tier limits (reference)

Limitfreestarterproscale
maxMonitors5502501,500
minIntervalSecs86,4003,600900300
retentionDays14180365730
maxDeliveriesPerMonitor131025
llmCallsPerMonth505005,00025,000
runNowPerDay5505005,000
proxyBytesPerMonth052,428,800524,288,0005,368,709,120
visualMonitors11075500

OpenAPI

The full OpenAPI 3.1 spec is served by the API itself:

GET /api/openapi.json

You can also browse it interactively at /docs on the dashboard host.

Error responses

All errors follow this format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable message",
    "details": [],
    "requestId": "string (optional)"
  }
}
CodeHTTP StatusMeaning
UNAUTHORIZED401Missing or invalid credentials
FORBIDDEN403Insufficient permissions
NOT_FOUND404Resource not found
VALIDATION_ERROR422Invalid request body
TIER_LIMIT_EXCEEDED429Plan limit reached
RATE_LIMIT_EXCEEDED429Too many requests
INTERNAL_ERROR500Server error