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

Create a monitor

POST /v1/monitors

FieldTypeRequiredDefaultConstraints / Allowed values
namestringyes1–200 characters
urlstringyesMust be a valid URL
schedule_interval_secondsintegeryesPositive integer; tier-gated minimum (free: 86400, starter: 3600, pro: 900, scale: 300)
extract_configobjectyesSee Extract config
diff_predicateobjectyesSee 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

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.

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": { "...": "..." },
  "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).

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"
}

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> }.

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
namestringyes1–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

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