API Documentation
This REST API reference covers URL inspection, batch checks, monitors, user-agent comparison, exports, billing, and account management. Most endpoints accept API keys and return JSON; public endpoints, user-session requirements, and other response formats are noted below. The full OpenAPI spec is available at /v1/openapi.yaml.
Base URL: https://api.checkredirects.io/v1
Authentication
For endpoints that accept API keys, send an Authorization header. Public endpoints need no key; Terms acceptance requires a direct user session instead.
Authorization: Bearer httpnd_your_api_key
Create API keys in Settings > API Keys in the web app, or via the API key endpoints. Keys use the httpnd_ prefix to distinguish them from session tokens.
Response Headers
Every authenticated API response includes monthly-capacity metadata. These headers do not describe the separate per-second request throttle; use GET /usage for your organization's effective runtime limits.
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000 X-Plan: pro X-Monthly-Limit: 15000 X-Monthly-Used: 4218 X-Monthly-Remaining: 10782 X-Credits-Remaining: 5000 X-RateLimit-Limit: 15000 X-RateLimit-Remaining: 10782 X-RateLimit-Reset: 1711929600
| Header | Description |
|---|---|
X-Request-ID | Unique ID for this request (useful for support) |
X-Plan | Your current plan: free, pro, or business |
X-Monthly-Limit | Total checks allowed this month |
X-Monthly-Used | Checks used so far this month |
X-Monthly-Remaining | Checks remaining this month |
X-Credits-Remaining | Purchased credit balance (used after monthly limit exhausted) |
X-RateLimit-Limit | Compatibility alias for X-Monthly-Limit; not the per-second request throttle |
X-RateLimit-Remaining | Compatibility alias for X-Monthly-Remaining |
X-RateLimit-Reset | Compatibility alias for the monthly reset timestamp |
MCP Server
If you use Claude, Cursor, or another MCP-compatible AI assistant, you can skip the REST API entirely. The checkredirects MCP server lets your AI call redirect checking tools directly.
go install github.com/neondeerdatalabs/[email protected]
Available tools: check_url, inspect_url, compare_agents, batch_check_and_wait, create_monitor, export_to_sheets, batch_results, list_monitors.
Full setup instructions, config examples, and source code: MCP Server page | GitHub
Inspect a URL
POST /inspect
Inspect a single URL. Returns the full redirect chain with status codes, headers, timing, resolved IPs, and TLS certificate data at every hop.
Request
curl -X POST https://api.checkredirects.io/v1/inspect \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://flatfile.io",
"method": "GET",
"follow_redirects": true,
"max_redirects": 10,
"user_agent": "Googlebot/2.1",
"headers": {"Accept-Language": "en-US"},
"cookies": "session=abc123",
"basic_auth": {"user": "admin", "pass": "secret"},
"preset_id": "uuid-of-saved-preset"
}' | Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to inspect (http/https only, max 2048 chars) |
method | string | No | HEAD (default) or GET |
follow_redirects | boolean | No | Follow redirects (default: true) |
max_redirects | integer | No | Max hops to follow (default: 10, max: 20) |
user_agent | string | No | Preset key (e.g. googlebot_desktop) or raw UA string. See catalog |
headers | object | No | Custom request headers (key-value pairs). Caller-supplied headers are sent only to the submitted origin and same-origin redirects. |
cookies | string | No | Cookie header value (e.g. "session=abc; token=xyz"). Sent only to the submitted origin and same-origin redirects. |
basic_auth | object | No | HTTP Basic Auth: {"user": "...", "pass": "..."}. Sent only to the submitted origin and same-origin redirects. |
retrieve_body | boolean | No | Parse final page for meta tags, canonical URL, OG tags (default: false). Paid plans only. Returns 403 upgrade_required on free plan. |
preset_id | string | No | Deprecated compatibility field; currently ignored. Send effective request options explicitly. |
Response: success (redirect chain resolving to 200)
{
"id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",
"original_url": "https://flatfile.io",
"normalized_url": "https://flatfile.io/",
"final_url": "https://flatfile.com/",
"final_status": 200,
"total_hops": 2,
"total_time_ms": 342,
"error": null,
"hops": [
{
"hop_number": 1,
"url": "https://flatfile.io/",
"method": "GET",
"status_code": 301,
"status_message": "Moved Permanently",
"resolved_ip": "76.76.21.21",
"time_ms": 128,
"headers": {
"Location": ["https://flatfile.com/"],
"Server": ["cloudflare"],
"Cf-Ray": ["8a1b2c3d4e5f-IAD"]
},
"tls": {
"version": "TLS 1.3",
"cipher": "TLS_AES_128_GCM_SHA256",
"certificate": {
"subject": "flatfile.io",
"issuer": "Let's Encrypt Authority X3",
"not_before": "2025-11-01T00:00:00Z",
"not_after": "2026-01-30T00:00:00Z",
"san": ["flatfile.io", "www.flatfile.io"],
"chain_valid": true
}
}
},
{
"hop_number": 2,
"url": "https://flatfile.com/",
"method": "GET",
"status_code": 200,
"status_message": "OK",
"resolved_ip": "76.76.21.22",
"time_ms": 214,
"headers": {
"Content-Type": ["text/html; charset=utf-8"],
"X-Frame-Options": ["DENY"],
"Strict-Transport-Security": ["max-age=31536000"]
},
"tls": {
"version": "TLS 1.3",
"cipher": "TLS_AES_256_GCM_SHA384",
"certificate": {
"subject": "flatfile.com",
"issuer": "DigiCert Inc",
"not_before": "2025-06-15T00:00:00Z",
"not_after": "2026-06-15T00:00:00Z",
"san": ["flatfile.com", "*.flatfile.com"],
"chain_valid": true
}
}
}
]
} Response: error (DNS failure)
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"original_url": "https://doesnt-exist-xyz.com",
"normalized_url": "https://doesnt-exist-xyz.com/",
"final_url": null,
"final_status": null,
"total_hops": 0,
"total_time_ms": 45,
"error": {
"code": "dns_failure",
"message": "DNS lookup failed: no such host"
},
"hops": []
} Response fields
| Field | Type | Description |
|---|---|---|
id | string | Unique check ID (UUID) |
original_url | string | URL as you submitted it |
normalized_url | string | URL after normalization (trailing slash, lowercased host) |
final_url | string | null | URL that returned a non-redirect status, or null on error |
final_status | integer | null | HTTP status code of the final response, or null on error |
total_hops | integer | Number of hops in the redirect chain |
total_time_ms | integer | Total wall-clock time in milliseconds |
error | object | null | Null on success. See inspection errors |
hops | array | Ordered list of hops (empty on pre-connection errors) |
Hop object
| Field | Type | Description |
|---|---|---|
hop_number | integer | 1-indexed position in the chain |
url | string | URL requested for this hop |
method | string | HTTP method used (HEAD or GET) |
status_code | integer | HTTP status code |
status_message | string | HTTP reason phrase (e.g. "Moved Permanently") |
resolved_ip | string | IP address the hostname resolved to |
time_ms | integer | Time for this hop in milliseconds |
headers | object or null | Response headers, with array values. Stored and cached results omit cookies, authentication headers, and other credential-bearing headers. Fresh uncached inspections may return them to an authorized caller. Public shares return null. |
tls | object | null | TLS info (null for plain HTTP) |
credentials_withheld | boolean | True when caller-supplied credentials were withheld from this hop after an origin change or HTTPS downgrade. |
credentials_withheld_reason | string | null | origin_change, scheme_downgrade, or unparsable_url; absent when credentials were not withheld. |
TLS / certificate object
| Field | Type | Description |
|---|---|---|
tls.version | string | TLS version (e.g. "TLS 1.3") |
tls.cipher | string | Cipher suite used |
tls.certificate.subject | string | Certificate subject (CN) |
tls.certificate.issuer | string | Issuing CA |
tls.certificate.not_before | string | Validity start (ISO 8601) |
tls.certificate.not_after | string | Validity end (ISO 8601) |
tls.certificate.san | string[] | Subject Alternative Names |
tls.certificate.chain_valid | boolean | Whether the full certificate chain validated |
Watch a URL
POST /watch
Fire-and-forget single-URL inspection. The request returns immediately with 202 accepted; the inspection runs in the background and the result is POSTed to your webhook_url (HMAC-signed with the org's webhook secret. Same verification flow as batch webhooks). Useful when you don't want to hold an HTTP connection open or you're driving inspections from a serverless function.
Request
curl -X POST https://api.checkredirects.io/v1/watch \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"webhook_url": "https://your-server.com/webhook",
"user_agent": "googlebot_desktop"
}' | Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to inspect (http/https, max 2048 chars) |
webhook_url | string | Yes | HTTPS URL to deliver the result to. Signed with X-Signature (HMAC-SHA256) |
user_agent | string | No | Catalog key or raw UA string |
Response: success (202)
{
"status": "accepted",
"watch_id": "9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d",
"message": "URL check started. Results will be sent to your webhook."
} Webhook payload (POST to your webhook_url)
{
"event": "watch.completed",
"watch_id": "9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d",
"url": "https://example.com",
"status": "completed",
"final_url": "https://example.com/",
"final_status": 200,
"total_hops": 1,
"timestamp": "2026-06-16T18:30:00Z"
} On failure, status is "failed" and an error_code field is set instead of final_url/final_status. See inspection errors for codes plus the two watch-specific codes below.
Billing
1 check unit is charged when the worker actually starts the inspection, not at request submission. That means:
- A
202 acceptedresponse does not guarantee a charge has been recorded yet. - If your capacity runs out between submission and worker pickup, the worker delivers a
watch.completedwebhook withstatus: "failed"anderror_code: "insufficient_capacity". No inspection runs, no charge. - If the dispatcher itself can't accept more work, the HTTP call fails fast with
503 rate_limitedand no webhook is sent.
Error responses
| HTTP | Code | When |
|---|---|---|
| 422 | validation_error | Missing or invalid url / webhook_url (must be https://). |
| 429 | rate_limited | Per-second API rate limit, or zero capacity available at submission time. |
| 503 | rate_limited | Watch dispatcher is at capacity. Retry shortly. |
Batch Inspect
POST /batch
Submit multiple URLs for asynchronous processing. The same request configuration (method, headers, user agent, etc.) is applied to all URLs in the batch.
Request
curl -X POST https://api.checkredirects.io/v1/batch \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com", "https://flatfile.io"],
"method": "GET",
"user_agent": "Googlebot/2.1",
"cookies": "session=abc",
"basic_auth": {"user": "admin", "pass": "secret"}
}' | Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | Yes | URLs to inspect. Use GET /usage to read the organization's effective maximum; it may differ from the standard plan default. |
method | string | No | HEAD (default) or GET |
follow_redirects | boolean | No | Follow redirects (default: true) |
max_redirects | integer | No | Max hops (default: 10, max: 20) |
user_agent | string | No | Custom User-Agent |
headers | object | No | Custom headers for each submitted URL's origin and same-origin redirects; withheld on HTTPS-to-HTTP downgrades. |
cookies | string | No | Cookie header value, with the same origin and downgrade restrictions. |
basic_auth | object | No | HTTP Basic Auth, with the same origin and downgrade restrictions. |
retrieve_body | boolean | No | Parse final page for meta tags (default: false). Paid plans only; Free returns 403 upgrade_required. |
preset_id | string | No | Associates an organization-owned preset for metadata only. Send effective request options explicitly. |
webhook_url | string | No | URL for a batch completion notification. Publicly reachable HTTPS destination; all plans. See webhooks. |
Response
{
"job_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status": "pending",
"total_urls": 2
} Batch job lifecycle
pending → running → completedA job is
completed when all URLs have been processed, even if some individual URLs failed.
There is no separate "partially_failed" status. Check failed_urls in the progress response to see how many had errors.
Individual check results will have error set to null (success) or an error object (failure).
GET /batch/{id}/progress
Poll for batch completion. This is a lightweight endpoint. Call it every 1-2 seconds.
curl https://api.checkredirects.io/v1/batch/JOB_ID/progress \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"status": "running",
"total_urls": 50,
"completed_urls": 32,
"failed_urls": 1
} | Field | Type | Description |
|---|---|---|
status | string | pending, running, or completed |
total_urls | integer | Total URLs in the batch |
completed_urls | integer | URLs that completed successfully |
failed_urls | integer | URLs that had errors (DNS failure, timeout, etc.) |
running. You'll get whatever has completed so far. No need to wait for completed status.
GET /batch/{id}
Get paginated results. Each item in checks has the same shape as a single inspect response.
curl "https://api.checkredirects.io/v1/batch/JOB_ID?page=1&per_page=25" \ -H "Authorization: Bearer httpnd_your_key"
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
per_page | integer | 50 | Results per page (max 100) |
status | string | — | Filter by status codes, comma-separated (e.g. 301,404) |
error | boolean | — | Set to true to show only failed checks |
sort | string | — | Sort by: latency, status, hops |
order | string | asc | asc or desc |
Response
{
"job_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status": "completed",
"total_urls": 50,
"completed_urls": 48,
"failed_urls": 2,
"page": 1,
"per_page": 25,
"total_count": 50,
"checks": [
{
"id": "...",
"original_url": "https://example.com",
"normalized_url": "https://example.com/",
"final_url": "https://example.com/",
"final_status": 200,
"total_hops": 1,
"total_time_ms": 89,
"error": null,
"hops": [...]
}
]
} | Field | Type | Description |
|---|---|---|
job_id | string | Batch job ID |
status | string | Job status |
total_urls | integer | Total URLs submitted |
completed_urls | integer | Successfully completed |
failed_urls | integer | Failed with errors |
page | integer | Current page number |
per_page | integer | Results per page |
total_count | integer | Total results matching your filters |
checks | array | Array of inspect response objects |
GET /batch/{id}/export/csv
Download batch results as a CSV file. Returns Content-Type: text/csv with a Content-Disposition header.
curl -O -J "https://api.checkredirects.io/v1/batch/JOB_ID/export/csv" \ -H "Authorization: Bearer httpnd_your_key"
POST /batch/{id}/export/sheets
Export batch results to a connected Google Sheets account. Requires a Google Sheets connection.
curl -X POST "https://api.checkredirects.io/v1/batch/JOB_ID/export/sheets" \ -H "Authorization: Bearer httpnd_your_key"
User-Agent Catalog
GET /user-agents
Returns the full catalog of available user-agent presets and comparison packs. Public endpoint. No auth required.
curl https://api.checkredirects.io/v1/user-agents
The response includes two arrays: agents (individual user-agents with keys, labels, groups, and full strings) and packs (preset groups for comparison mode).
Using preset keys
Anywhere the API accepts a user_agent field, you can pass either a preset key (e.g. googlebot_desktop) or a raw UA string. If it matches a known key, the full string is used. Otherwise it's treated as a literal.
// Using a preset key
{"url": "https://example.com", "user_agent": "googlebot_desktop"}
// Using a raw string
{"url": "https://example.com", "user_agent": "MyCustomBot/1.0"} Available groups
| Group | Example keys |
|---|---|
| Search Engine Crawlers | googlebot_desktop, googlebot_mobile, bingbot_desktop, yandexbot |
| Social Media Crawlers | facebook, twitterbot, linkedin, slackbot, discord |
| Desktop Browsers | chrome_windows, chrome_mac, firefox_windows, safari_mac, edge_windows |
| Mobile Browsers | chrome_android, safari_iphone, safari_ipad, samsung_browser |
| AI Crawlers | gptbot, chatgpt_user, claudebot, google_extended, bytespider |
| Utility | curl, wget, empty |
User-Agent Comparison
POST /compare-agents
Inspect a single URL with multiple user-agents simultaneously and compare the responses. Flags whether the responses differ (different status code or final URL). Available on all plans. Each user-agent counts as one check.
With explicit user-agents
curl -X POST https://api.checkredirects.io/v1/compare-agents \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "user_agents": ["googlebot_desktop", "chrome_windows", "facebook"]}' With a preset pack
curl -X POST https://api.checkredirects.io/v1/compare-agents \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "pack": "seo_essentials"}' | Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to inspect |
user_agents | string[] | * | List of UA keys or raw strings (max 10) |
pack | string | * | Preset pack key (alternative to user_agents) |
method | string | No | HEAD or GET (default: GET) |
max_redirects | integer | No | Maximum redirects to follow (default: 10, max: 20) |
follow_redirects | boolean | No | Follow redirects (default: true) |
retrieve_body | boolean | No | Parse final page for meta tags (default: false). Paid plans only; Free returns 403 upgrade_required. |
Provide either user_agents or pack, not both.
Available packs
| Key | Name | Agents | Use case |
|---|---|---|---|
seo_essentials | SEO Essentials | 5 | Do search engines see the same thing as browsers? |
social_preview | Social Preview Check | 6 | Will shared links look right on every platform? |
mobile_vs_desktop | Mobile vs Desktop | 6 | Catch m-dot redirects and device-based routing |
ai_crawler_audit | AI Crawler Audit | 6 | Are your AI blocking rules actually working? |
full_coverage | Full Coverage | 10 | Check everything. Search, browsers, social, AI |
Response
{
"url": "https://example.com",
"differ": true,
"results": [
{
"user_agent_key": "googlebot_desktop",
"user_agent_label": "Googlebot Desktop",
"user_agent_string": "Mozilla/5.0 ...",
"final_url": "https://example.com/",
"final_status": 200,
"total_hops": 1,
"total_time_ms": 142,
"error": null,
"hops": [...]
},
{
"user_agent_key": "safari_iphone",
"user_agent_label": "Safari (iPhone)",
"user_agent_string": "Mozilla/5.0 (iPhone; ...",
"final_url": "https://m.example.com/",
"final_status": 200,
"total_hops": 2,
"total_time_ms": 186,
"error": null,
"hops": [...]
}
]
} Monitors
Monitors run a saved batch on a recurring cadence (minimum every 30 minutes), POST results to your webhook on each run, and optionally append each run to a Google Spreadsheet. They auto-pause after 5 consecutive failed runs. Use reset-failures to clear the counter. Paid plans only.
GET /monitors
List all monitors in the organization.
curl "https://api.checkredirects.io/v1/monitors?page=1&per_page=20" \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"data": [{...monitor...}],
"total": 4,
"page": 1,
"per_page": 20
} POST /monitors
Create a new monitor. Each URL counts toward the per-monitor URL cap (see plan limits). Plan caps the number of monitors per org (Pro: 50, Business: 250).
curl -X POST https://api.checkredirects.io/v1/monitors \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Homepage canary",
"urls": ["https://example.com", "https://example.com/about"],
"interval_minutes": 60,
"method": "GET",
"user_agent": "googlebot_desktop",
"webhook_url": "https://your-server.com/monitor-webhook",
"sheets_append": false
}' | Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the monitor |
urls | string[] | * | Simple list of URLs. Use either urls or items, not both. |
items | object[] | * | Alternative to urls with per-URL passthrough data: [{"url": "...", "data": {...}}]. The data object is included in every run's results and webhook payloads. Max 64KB per URL. |
interval_minutes | integer | Yes | How often to run. Minimum 30. |
preset_id | string | No | Associates an organization-owned preset for metadata only. Send effective request options explicitly. |
method | string | No | HEAD (default) or GET |
follow_redirects | boolean | No | Follow redirects (default true) |
max_redirects | integer | No | Max hops (default 10, max 20) |
user_agent | string | No | Catalog key or raw UA string |
headers | object | No | Custom request headers |
cookies | string | No | Cookie header value |
webhook_url | string | No | HTTPS URL to POST each run's result to. Signed identically to batch webhooks. |
sheets_append | boolean | No | If true, auto-create a spreadsheet on the first run and append each subsequent run. Requires a connected Google Sheets account. |
Monitor object
{
"id": "uuid",
"name": "Homepage canary",
"urls": ["https://example.com"],
"interval_minutes": 60,
"request_config": {"method": "GET", "user_agent": "..."},
"has_credentials": {"headers": true, "cookies": false},
"status": "active",
"webhook_url": "https://your-server.com/monitor-webhook",
"sheets_append": false,
"sheets_spreadsheet_id": null,
"last_run_at": "2026-06-16T18:00:00Z",
"next_run_at": "2026-06-16T19:00:00Z",
"last_job_id": "job-uuid",
"consecutive_failures": 0,
"badge_url": "https://api.checkredirects.io/v1/badge/uuid/SIGNED",
"created_by": "user-uuid",
"created_at": "2026-06-01T00:00:00Z",
"updated_at": "2026-06-16T18:00:00Z"
} | Field | Notes |
|---|---|
status | active, paused, or deleted |
consecutive_failures | Resets to 0 on success. Monitor auto-pauses at 5. |
badge_url | Pre-signed public URL for an embeddable SVG status badge. See Status Badge. Omitted when the server has no badge-signing secret configured. |
sheets_spreadsheet_id | The auto-created spreadsheet ID after the first sheets-append run. |
request_config | Operational request settings only. Stored header and cookie values are write-only and never returned. |
has_credentials | Boolean markers showing whether supported write-only credential kinds are configured, without exposing their values. |
GET /monitors/{id}
Get a single monitor by ID.
PUT /monitors/{id}
Full operational update. name and interval_minutes are required. Omit preset_id to clear its metadata-only association. Omit headers or cookies to preserve the stored write-only value; send an empty object or empty string to clear it.
DELETE /monitors/{id}
Soft-delete a monitor. Admin role required.
{"status": "deleted"} POST /monitors/{id}/pause
Pause scheduling. The monitor will not run again until resumed.
{"status": "paused"} POST /monitors/{id}/resume
Resume a paused monitor. The next run is scheduled on the next scheduler tick (within ~1 minute).
{"status": "active"} POST /monitors/{id}/trigger
Manually trigger an immediate run. The HTTP call returns 200 as soon as the run is scheduled; billing happens when the scheduler executes the run. If there is no capacity at that point, the scheduler logs a skip. The trigger endpoint itself still returns 200.
{"status": "triggered"} POST /monitors/{id}/reset-failures
Clears consecutive_failures and any degraded badge state. Use after fixing whatever was breaking the monitor so the next run starts from a clean slate. Does not delete the run history. Member role required.
{"status": "reset"} GET /monitors/{id}/runs
List the batch jobs spawned by this monitor, newest first.
curl "https://api.checkredirects.io/v1/monitors/MONITOR_ID/runs?page=1&per_page=20" \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"data": [
{
"job_id": "job-uuid",
"status": "completed",
"total_urls": 2,
"completed_urls": 2,
"failed_urls": 0,
"created_at": "2026-06-16T18:00:00Z",
"completed_at": "2026-06-16T18:00:02Z"
}
],
"total": 137,
"page": 1,
"per_page": 20
} To read the per-URL results for a run, pass job_id to GET /batch/{id}.
Monitor Bulk Operations
Three bulk endpoints share the same response shape and per-item error reporting. Useful for migrating a list of URLs into monitors in one round-trip, or pausing every monitor before a planned maintenance window.
Bulk response shape
{
"total_requested": 3,
"succeeded": 2,
"failed": 1,
"results": [
{"index": 0, "status": "created", "monitor": {...monitor...}},
{"index": 1, "status": "created", "monitor": {...monitor...}},
{"index": 2, "status": "error", "error": {"code": "validation_error", "message": "interval_minutes must be at least 30"}}
]
} For pause/resume, each results[] entry has id (the monitor UUID) instead of index, and successful items use "status": "paused" or "resumed" instead of "created". The whole call returns 200 even when individual items fail. Check failed and per-item error objects. Each bulk call accepts a maximum of 50 IDs (pause/resume) or 25 monitor bodies (create).
POST /monitors/bulk
Create up to 25 monitors in one call. Each item uses the same body as POST /monitors. Plan-level monitor caps still apply. The operation rejects the whole batch with 403 upgrade_required if accepting the new monitors would exceed your plan limit.
curl -X POST https://api.checkredirects.io/v1/monitors/bulk \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"monitors": [
{"name": "/about", "urls": ["https://example.com/about"], "interval_minutes": 60},
{"name": "/pricing", "urls": ["https://example.com/pricing"], "interval_minutes": 60}
]
}' POST /monitors/bulk/pause
Pause multiple monitors by ID.
curl -X POST https://api.checkredirects.io/v1/monitors/bulk/pause \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"ids": ["monitor-uuid-1", "monitor-uuid-2"]}' POST /monitors/bulk/resume
Resume multiple monitors by ID. Same request shape as bulk pause.
Status Badge
GET /badge/{id}/{sig}
Returns a public SVG badge showing the monitor's current status. Suitable for embedding in a README, status page, or runbook. No authentication required; the URL is HMAC-signed so knowing a monitor UUID alone isn't enough to query it.
Use the badge_url field from any monitor response. The server emits it pre-signed for the correct host (the API origin, e.g. https://api.checkredirects.io, not the app origin). Requests without a valid signature render a generic "invalid signature" SVG (not a 404) so a broken README image still tells you what went wrong.
Example embed:
<img src="https://api.checkredirects.io/v1/badge/MONITOR_ID/SIGNATURE"
alt="Homepage canary status"> Cache-Control: public, max-age=300, s-maxage=300. Expect up to ~5 minutes of staleness in embedded images after a status change.
Sitemap Import
GET /sitemap/parse?url=...
Fetches and parses an XML sitemap, returning the extracted URLs. Supports both <urlset> and <sitemapindex> formats. Use the returned URLs as input to the batch endpoint.
curl "https://api.checkredirects.io/v1/sitemap/parse?url=https://example.com/sitemap.xml" \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"url": "https://example.com/sitemap.xml",
"url_count": 47,
"urls": [
"https://example.com/",
"https://example.com/about",
"https://example.com/pricing",
...
]
} Job History
GET /jobs
Returns your organization's inspection job history, newest first. For single-URL jobs, includes the first check's URL and final status for display.
curl "https://api.checkredirects.io/v1/jobs?page=1&per_page=20" \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"jobs": [
{
"id": "job-uuid",
"job_type": "single",
"status": "completed",
"total_urls": 1,
"completed_urls": 1,
"failed_urls": 0,
"created_at": "2026-03-23T10:30:00Z",
"first_url": "https://flatfile.io",
"final_status": 200,
"first_check_id": "check-uuid"
},
{
"id": "job-uuid-2",
"job_type": "batch",
"status": "completed",
"total_urls": 50,
"completed_urls": 48,
"failed_urls": 2,
"created_at": "2026-03-22T14:00:00Z"
}
],
"page": 1,
"per_page": 20,
"total_count": 42
} Job Comparison (Diff)
GET /jobs/{id}/compare/{other_id}
Compare two batch jobs side-by-side. Matches checks by original_url and flags changes in status code, final URL, or hop count. Useful for tracking migration progress or monitoring changes over time.
curl "https://api.checkredirects.io/v1/jobs/JOB_A_ID/compare/JOB_B_ID" \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"job_a": "job-a-uuid",
"job_b": "job-b-uuid",
"summary": {
"total_urls": 50,
"changed": 3,
"unchanged": 45,
"added_in_b": 1,
"removed_from_a": 1
},
"diffs": [
{
"url": "https://example.com/old-page",
"change": "changed",
"status_a": 301,
"status_b": 200,
"final_url_a": "https://example.com/new-page",
"final_url_b": "https://example.com/old-page",
"hops_a": 2,
"hops_b": 1,
"time_ms_a": 245,
"time_ms_b": 89
},
{
"url": "https://example.com/removed",
"change": "removed",
"status_a": 200,
"final_url_a": "https://example.com/removed"
}
]
} | Change value | Meaning |
|---|---|
unchanged | Same status, final URL, and hop count |
changed | Status code, final URL, or hop count changed |
added | URL exists in job B but not job A |
removed | URL exists in job A but not job B |
Shareable Results
GET /results/{check_id}
Retrieve a shared inspection result by check ID without authentication. The containing job must be explicitly shared; private, revoked, expired, or missing results return 404.
curl https://api.checkredirects.io/v1/results/CHECK_UUID
The web app uses app.checkredirects.io/r/{check_id}. Both this endpoint and public batch results return a reduced inspection response:
- URL fields and redirect-chain entries omit user information, query strings, and fragments. Paths remain visible.
- Each check omits
job_idandpassthrough_data. Hopheadersandtlsarenull;resolved_ip,geo, andmetaare omitted. - Hop
dns_ms,connect_ms,tls_ms, andttfb_msare zero. Total duration and hoptime_msremain. - Hop
status_messageis empty. Inspection error codes remain, but their messages are generic.
Authenticated result reads, CSV exports, and Sheets exports do not use this public-share redaction. Review exported URLs and metadata before forwarding them.
Result Retention
Saved results, including public shares, use the organization's plan when the job was created. Later plan changes do not change that job's retention periods. Periods run from job creation.
| Plan | Redirect-hop detail | Result summaries and jobs |
|---|---|---|
| Free | 7 days | 30 days |
| Pro | 30 days | 90 days |
| Business | 90 days | 6 months |
Scheduled cleanup removes expired data from completed, failed, and cancelled jobs. Hop detail can expire before the summary, leaving an empty hops array. Once the result or job is deleted, its read and share URLs return 404. Sharing does not extend retention; export results you need to keep.
Batch Webhooks
When submitting a batch job, include a webhook_url to receive a POST notification when the batch completes. Available on every plan. The destination must be a publicly reachable HTTPS URL; invalid or prohibited destinations return 422 validation_error.
// Submit batch with webhook
curl -X POST https://api.checkredirects.io/v1/batch \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com"], "webhook_url": "https://your-server.com/webhook"}' Webhook payload (POST to your URL)
{
"event": "batch.completed",
"job_id": "job-uuid",
"status": "completed",
"total_urls": 50,
"completed_urls": 48,
"failed_urls": 2,
"timestamp": "2026-03-23T10:35:00Z"
} Verifying webhook signatures
Each webhook includes an X-Signature header containing an HMAC-SHA256 signature of the request body. The full webhook signing secret is delivered once by POST /v1/org/webhook-secret/rotate (admin role required) and shown once in Settings. GET /v1/org/webhook-secret only returns a non-revealing preview (first 8 chars + length) so the value cannot be exfiltrated by a stolen viewer-level session. If you lose the secret you have to rotate to get a new one. Rotation immediately invalidates the previous secret. Any in-flight webhooks signed with the old secret will fail verification. Pause monitors before rotating if you need zero missed deliveries.
// Node.js verification example
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
const isValid = verifyWebhook(rawBody, req.headers['x-signature'], WEBHOOK_SECRET); # Python verification example
import hmac, hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected) User-Agent: checkredirects.io/webhook and X-Signature headers.
Delivery status and errors
Authenticated GET /batch/{id} responses include webhook once a delivery result is recorded. Its status is delivered or failed. The optional delivered_at timestamp records the outcome, including a failed attempt; it does not by itself mean delivery succeeded.
// Webhook field within a batch response
"webhook": {
"status": "failed",
"delivered_at": "2026-09-26T12:00:00Z",
"error": "webhook_http_error_503"
} webhook.error is an optional diagnostic code, not a receiver URL or raw transport error. Check status even when error is absent.
| Code | Meaning |
|---|---|
webhook_http_error_<status> | The receiver returned an unsuccessful HTTP status, such as webhook_http_error_503. |
webhook_timeout, webhook_network_error | Delivery timed out or the receiver could not be reached. |
webhook_url_invalid, webhook_redirect_rejected | The destination was rejected or attempted a redirect. |
webhook_payload_error, webhook_signing_error, webhook_request_error | The payload, signature, or request could not be prepared. |
webhook_canceled, webhook_delivery_error | Delivery was cancelled or failed for another reason. |
Batch Sharing
Turn a completed batch into a public, no-auth-required link. Useful for sharing migration reports with clients or embedding QA results in a ticket.
POST /batch/{id}/share
Mark the batch as publicly accessible at /b/{id}. Member role required.
curl -X POST https://api.checkredirects.io/v1/batch/JOB_ID/share \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"is_public": true,
"share_url": "https://app.checkredirects.io/b/JOB_ID"
} DELETE /batch/{id}/share
Revoke public access. The link returns 404 to unauthenticated callers immediately.
curl -X DELETE https://api.checkredirects.io/v1/batch/JOB_ID/share \ -H "Authorization: Bearer httpnd_your_key"
Response
{"is_public": false} GET /batch/{id}/public
Read a shared batch without authentication. Returns batch counts and paginated checks using the public-share redaction; webhook diagnostics and batch statistics are omitted. Returns 404 when the batch is private, revoked, expired, or missing. Result retention still applies.
curl "https://api.checkredirects.io/v1/batch/JOB_ID/public?page=1&per_page=25"
Webhook Secrets
Manage the signing secret used to verify webhook payloads. See Batch Webhooks for verification examples.
GET /org/webhook-secret
Returns a preview of the current webhook signing secret. The first 8 characters plus the total length. The full value is never returned by this endpoint; rotate to receive a fresh secret in plaintext. Admin role required.
curl https://api.checkredirects.io/v1/org/webhook-secret \ -H "Authorization: Bearer httpnd_your_key"
Response
{"preview": "a1b2c3d4", "length": 64} POST /org/webhook-secret/rotate
Generate a new webhook signing secret. The previous secret is immediately invalidated. The full value is returned once; store it before navigating away. Admin role required.
curl -X POST https://api.checkredirects.io/v1/org/webhook-secret/rotate \ -H "Authorization: Bearer httpnd_your_key"
Response
{"webhook_secret": "e5f6a7b8..."} Test a Webhook
POST /webhook-test
Sends a signed test payload to a URL so you can verify your signature-verification implementation before pointing real traffic at it. The request is HMAC-signed with your org's current webhook secret. Member role required.
curl -X POST https://api.checkredirects.io/v1/webhook-test \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-server.com/webhook"}' | Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to deliver the test payload to. SSRF-validated (private/internal IPs rejected). |
Response: success
{"status": "sent", "message": "Test webhook delivered successfully."} Error responses
| HTTP | Code | When |
|---|---|---|
| 422 | validation_error | url is missing, not HTTPS, or fails SSRF validation. |
| 502 | webhook_failed | The remote endpoint did not respond with a 2xx status. |
MCP Ping
POST /mcp/ping
Connectivity probe used by the MCP server to confirm the API is reachable. Any valid bearer token works (viewer role and up).
curl -X POST https://api.checkredirects.io/v1/mcp/ping \ -H "Authorization: Bearer httpnd_your_key"
Response
{"status": "ok"} Presets
Presets save non-sensitive request configurations (method, user agent, and redirect behavior) for reuse in the web app. The API accepts only the four documented fields; headers, cookies, authentication data, and arbitrary fields are rejected. A preset_id on batch and monitor requests records which saved configuration was selected, but API clients must send the effective request options explicitly. Requires a paid plan.
GET /presets
List all presets for your organization.
curl https://api.checkredirects.io/v1/presets \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"data": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-234567890abc",
"name": "Googlebot",
"description": "Inspect as Googlebot with US English",
"config": {
"method": "GET",
"user_agent": "Googlebot/2.1 (+http://www.google.com/bot.html)",
"follow_redirects": true,
"max_redirects": 10
},
"is_default": false,
"created_by": "user-uuid",
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
],
"total": 1
} POST /presets
Create a new preset.
curl -X POST https://api.checkredirects.io/v1/presets \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Googlebot",
"description": "Inspect as Googlebot with US English",
"config": {
"method": "GET",
"user_agent": "Googlebot/2.1 (+http://www.google.com/bot.html)",
"follow_redirects": true,
"max_redirects": 10
}
}' | Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Preset name |
description | string | No | Optional description |
config | object | No | Closed request config: method, user_agent, follow_redirects, and max_redirects only |
GET /presets/{id}
Get a single preset by ID.
PUT /presets/{id}
Update a preset. Same request body as POST. Members can only edit their own presets; admins can edit any.
DELETE /presets/{id}
Delete a preset. Requires admin role.
Usage
GET /usage
Returns your organization's current usage and effective runtime limits. Numeric admin overrides are organization-wide, so API keys and browser sessions for the same organization share these values.
curl https://api.checkredirects.io/v1/usage \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"monthly_limit": 15000,
"used_this_month": 4218,
"used_today": 142,
"plan": "pro",
"check_credits": 5000,
"reward_credits": 0,
"effective_limits": {
"monthly_checks": 15000,
"batch_size": 200,
"max_inflight_urls": 500,
"max_concurrent_batches": 3,
"api_requests_per_sec": 25,
"max_monitors": 50
}
} batch_size is the static maximum for a new batch after the organization, server, and in-flight thresholds are reconciled. Work already in flight can temporarily reduce immediate capacity, and concurrent submissions can briefly pass the in-flight URL or concurrent-batch thresholds, so callers must still handle 429 responses. api_requests_per_sec is an organization-shared one-second sliding-window throttle on protected execution routes, not a guaranteed per-minute allowance. max_monitors includes plan entitlement; a value of zero means monitor creation is unavailable.
GET /usage/history
Returns daily usage for the last 90 days. Requires admin role.
curl https://api.checkredirects.io/v1/usage/history \ -H "Authorization: Bearer httpnd_your_key"
Response
[
{"date": "2026-03-23", "checks_used": 142},
{"date": "2026-03-22", "checks_used": 87},
...
] Billing
Billing endpoints manage Stripe subscriptions and credit purchases. These are typically used from the web app, but are available via the API. Owner role required.
GET /billing/status
Returns your current plan, subscription status, and credit balance.
curl https://api.checkredirects.io/v1/billing/status \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"plan": "pro",
"stripe_subscription_id": "sub_...",
"period_end": "2026-04-15T00:00:00Z",
"check_credits": 5000
} POST /billing/checkout
Start a plan change for your organization. Redirect the user to redirect_url.
If your organization has no active subscription, action is "checkout" and redirect_url is a Stripe-hosted checkout page. If your organization already has an active subscription, action is "updated": the plan is changed in place on the existing subscription (proration is added to the next invoice by default) and no new Stripe session is created.
curl -X POST https://api.checkredirects.io/v1/billing/checkout \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"plan": "pro"}' Response
{"action": "checkout", "redirect_url": "https://checkout.stripe.com/..."} POST /billing/credits
Purchase a credit pack (10,000 checks for $10). Credits are used when your monthly limit is exhausted. In a Free organization, purchased credits expire after two years without being added or used; they do not expire while the organization is on a paid plan.
curl -X POST https://api.checkredirects.io/v1/billing/credits \ -H "Authorization: Bearer httpnd_your_key"
Response
{"checkout_url": "https://checkout.stripe.com/..."} POST /billing/portal
Creates a Stripe Customer Portal session to manage your subscription, payment method, and invoices.
Response
{"portal_url": "https://billing.stripe.com/..."} Organization & Team
Manage your organization settings and team members. Team management requires a paid plan.
GET /org
Get organization details.
{
"id": "org-uuid",
"name": "Acme Corp",
"slug": "acme-corp",
"plan": "pro",
"support_access_enabled": false
} PUT /org
Update organization name. Requires admin role.
{"name": "New Name"} GET /org/members
List all team members with their roles.
[
{"id": "user-uuid", "email": "[email protected]", "name": "Alice", "role": "owner"},
{"id": "user-uuid", "email": "[email protected]", "name": "Bob", "role": "member"}
] POST /org/invite
Invite a new member. Requires admin role + paid plan.
{"email": "[email protected]", "role": "member"} PUT /org/members/{user_id}/role
Change a member's role. Requires admin role + paid plan.
{"role": "admin"} DELETE /org/members/{user_id}
Remove a member from the organization. Requires admin role + paid plan.
Roles
| Role | Inspect | Batch | Manage Team | Manage Billing | Admin Settings |
|---|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes | Yes |
| Admin | Yes | Yes | Yes | No | Yes |
| Member | Yes | Yes | No | No | No |
| Viewer | Read-only | Read-only | No | No | No |
Org Settings
Toggles that change how the org is treated by the platform. All require admin (or owner, where noted) and are blocked while you're being impersonated by support.
PUT /org/support-access
Allow or revoke support access for troubleshooting, including support impersonation. Grants expire after 48 hours. Owner role required.
curl -X PUT https://api.checkredirects.io/v1/org/support-access \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"enabled": true}' PUT /org/data-opt-out
Control optional organization data collection. Available on every plan; admin role required. The opt_out boolean is required. GET /org also reports dpa_restricted; when true, setting opt_out:false returns 409 dpa_collection_restricted. Enabling opt-out remains allowed.
{"opt_out": true} Response
{"opt_out": true} PUT /org/auto-export-sheets
When enabled, every inspection and batch automatically appends to the org's default Google Spreadsheet. Requires a connected Google Sheets account. Paid plans only; admin role required.
{"enabled": true} Response
{"auto_export_sheets": true} DELETE /org
Owner only. Stops organization work, cancels the recorded subscription, and verifies billing before permanently deleting the organization's primary application records and stored credentials. This cannot be undone. It does not automatically erase sign-in accounts, provider-held records, or backups; see deletion boundaries in the Privacy Policy.
{"status": "deleted"} | Response | What to do |
|---|---|
409 billing_out_of_sync | No application data was deleted. Follow the response's retry or support guidance for the billing conflict. |
502 billing_unavailable | Billing cancellation or verification failed; no application data was deleted. Retry or contact support. |
503 organization_drain_timeout, organization_unavailable, or billing_disabled | Deletion could not proceed. Retry later. |
503 deletion_outcome_unavailable | Deletion may have completed. Contact support before retrying. |
Subscription cancellation may succeed even if a later deletion step fails. A timeout or 500 response is not confirmation that data remains or that deletion succeeded; contact support if the outcome is unclear.
Terms Acceptance
API keys can read organization acceptance status, but cannot accept Terms on a person's behalf. Acceptance requires an explicit action by a directly signed-in organization owner. Impersonation and platform-admin sessions cannot accept.
| Endpoint | Purpose |
|---|---|
GET /terms/current | Public metadata: current version, content_hash, acceptance_enabled, signup_required, update_summary, and notice_hash. |
GET /terms/versions/{version} | Public, exact published document text. Unpublished versions return 404. |
GET /org/terms | Organization status, including accepted_at, notice_displayed_at, and can_accept. Viewer role or higher. |
POST /org/terms/accept | Direct owner session only. Send accepted_terms:true, terms_version, and terms_content_hash for the document shown. Repeating acceptance preserves the first timestamp. |
POST /org/terms/notice | Direct owner session only. Record display of the notice using terms_version, terms_content_hash, and notice_hash. This is not acceptance. |
When acceptance is enabled, signup and invitation acceptance include accepted_terms, terms_version, and terms_content_hash from the document shown. Server-side acceptance is required when signup_required is true. Invitation acceptance records the individual's agreement, not an owner's agreement for the organization. Link the Privacy Policy separately.
On 409 terms_changed or terms_disabled, refresh metadata and the consent screen; never substitute a new version and resubmit acceptance automatically. These APIs do not themselves suspend existing API keys or establish legal effective dates. See the OpenAPI request and response schemas for full details.
API Key Management
Manage API keys programmatically. Requires admin role.
GET /api-keys
List all API keys for your organization. The key value is not included. It's only shown once at creation.
[
{
"id": "key-uuid",
"name": "CI Pipeline",
"prefix": "httpnd_a1b2",
"created_by": "user-uuid",
"created_at": "2026-01-10T08:00:00Z",
"last_used_at": "2026-03-22T14:30:00Z",
"expires_at": null
}
] POST /api-keys
Create a new API key. The full key is returned only in this response. Store it securely.
curl -X POST https://api.checkredirects.io/v1/api-keys \
-H "Authorization: Bearer httpnd_your_key" \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline"}' Response
{
"id": "key-uuid",
"name": "CI Pipeline",
"key": "httpnd_a1b2c3d4e5f6g7h8i9j0..."
} DELETE /api-keys/{id}
Revoke an API key immediately.
Rewards
One-shot actions that grant reward credits when completed (e.g. running your first batch or connecting Google Sheets). Reward credits are used before your monthly allowance and purchased credits, and expire after 30 days without being added or used.
GET /rewards
List every reward and whether your org has claimed it. Viewer role or above.
curl https://api.checkredirects.io/v1/rewards \ -H "Authorization: Bearer httpnd_your_key"
Response
{
"rewards": [
{
"key": "try_batch",
"title": "Run your first batch",
"description": "Run a batch check and we'll cover it plus 10 bonus credits.",
"credits": 10,
"max_claims": 1,
"claimed": true,
"claim_count": 1
},
{
"key": "connect_sheets",
"title": "Connect Google Sheets",
"description": "Connect your Google Sheets account and earn 25 credits.",
"credits": 25,
"max_claims": 1,
"claimed": false,
"claim_count": 0
}
],
"total_unclaimed_credits": 25
} Reward items may include require_plan (e.g. "pro") when the reward is plan-gated. Some rewards (like try_batch) calculate credits at claim time and may award the listed base plus a dynamic bonus.
Reward keys
| Key | What unlocks it |
|---|---|
try_batch | Run a batch check (dynamic: covers the batch plus 10 bonus credits) |
try_compare_agents | Run a user-agent comparison (Pro+; dynamic: covers the comparison plus 50 bonus credits) |
share_x | Share checkredirects.io on X |
share_linkedin | Share checkredirects.io on LinkedIn |
connect_sheets | Connect a Google Sheets account |
create_api_key | Create your first API key |
use_mcp | Make a request via the MCP server |
invite_member | Invite a teammate (Pro+) |
POST /rewards/{key}/claim
Claim a reward by its action key. For most rewards the API verifies the underlying action has actually happened (e.g. You can't claim try_batch without having run a batch). Idempotent: re-claiming a one-shot reward returns 422 already_claimed instead of double-granting credits. Member role required.
curl -X POST https://api.checkredirects.io/v1/rewards/try_batch/claim \ -H "Authorization: Bearer httpnd_your_key"
Response: success
{
"credits_awarded": 10,
"new_credit_balance": 5010,
"message": "You earned 10 credits!"
} Error responses
| HTTP | Code | When |
|---|---|---|
| 403 | upgrade_required | Reward is gated behind a plan tier the org isn't on. |
| 404 | not_found | Unknown reward key. |
| 422 | action_not_completed | The qualifying action hasn't actually happened yet. |
| 422 | already_claimed | Reward has already been claimed up to its max_claims limit. |
| 429 | rate_limited | Per-second API rate limit. |
Google Sheets Export
Connect your Google account to export batch results directly to Sheets.
GET /sheets/connect
Returns a Google OAuth URL. Redirect the user to this URL to initiate the connection flow.
GET /sheets/status
Check whether your org has a connected Google Sheets account.
{"connected": true, "email": "[email protected]"} DELETE /sheets/disconnect
Disconnect Google Sheets. Requires admin role.
POST /batch/{id}/export/sheets
Export a completed batch to a new Google Sheet. Requires member role and an active Sheets connection.
Rate Limits
Monthly inspection capacity and the per-second request throttle are separate controls. The legacy X-RateLimit-* response headers are compatibility aliases for monthly check capacity:
X-RateLimit-Limit: 15000 X-RateLimit-Remaining: 10782 X-RateLimit-Reset: 1711929600
| Header | Description |
|---|---|
X-RateLimit-Limit | Compatibility alias for your monthly check limit |
X-RateLimit-Remaining | Compatibility alias for checks remaining this month |
X-RateLimit-Reset | Compatibility alias for the monthly reset timestamp |
Call GET /usage to discover the effective api_requests_per_sec throttle and other runtime limits. The throttle is shared by the organization across its API keys and sessions and applies only to protected execution routes. A 429 response includes Retry-After; the compatibility headers do not report per-second remaining capacity.
Monthly check limits
| Plan | Checks/month | Batch size | API requests/sec |
|---|---|---|---|
| Free | 250 | 25 URLs | 10 |
| Pro ($8/mo) | 15,000 | 200 URLs | 25 |
| Business ($25/mo) | 100,000 | 500 URLs | 50 |
These are the standard plan defaults. Where an organization has an administrative override, GET /usage returns the value that the API actually enforces.
429 Too Many Requests. If you have check credits, they are consumed automatically and the request succeeds.
API Errors
All errors return a consistent JSON envelope with a request_id for debugging:
{
"error": {
"code": "rate_limited",
"message": "Monthly check limit exceeded. Purchase credits or upgrade your plan.",
"request_id": "7c3a1f2e-4b5d-4e6f-8a9b-0c1d2e3f4a5b"
}
} Rate limit errors (429) include a Retry-After header indicating how many seconds to wait before retrying.
| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Missing, invalid, or expired auth token / API key |
forbidden | 403 | Insufficient role permissions |
upgrade_required | 403 | Feature requires a paid plan (presets, team management, body retrieval) |
not_found | 404 | Resource not found (or not accessible by your org) |
validation_error | 422 | Invalid request body or parameters |
rate_limited | 429 | Monthly check limit exceeded, per-second API rate limit exceeded, or maximum concurrent batch jobs reached |
billing_disabled | 503 | Billing is not configured on this instance |
sheets_disabled | 503 | Google Sheets is not configured on this instance |
internal_error | 500 | Unexpected server error |
Inspection Errors
These error codes appear inside the error field of an inspect response. They describe what went wrong when checking a specific URL. Distinct from API errors which are HTTP-level failures.
| Code | Description |
|---|---|
dns_failure | DNS lookup failed. Hostname doesn't resolve |
connection_refused | TCP connection was refused by the remote server |
connection_timeout | TCP connection timed out (couldn't reach the server) |
tls_error | TLS handshake failed (generic) |
tls_cert_expired | Server's TLS certificate has expired |
tls_cert_invalid | Server's TLS certificate is invalid (wrong hostname, untrusted CA, etc.) |
response_timeout | Server accepted connection but didn't respond in time |
body_timeout | Response body download timed out |
too_many_redirects | Redirect chain exceeded the max_redirects limit (chain of distinct URLs) |
redirect_loop | Redirect chain revisited a URL it had already seen. The site is actually looping, not just verbose |
ssrf_blocked | URL resolved to a private/internal IP address (SSRF protection) |
url_blocked | Historical result from the removed Google Safe Browsing integration. New inspections do not produce this code. |
invalid_redirect | Redirect Location header was missing or malformed |
response_too_large | Response body exceeded 256 KB limit |
protocol_error | HTTP protocol violation or malformed response |
unknown | Unclassified error. See the message field for details |
POST /inspect that encounters a DNS failure still returns HTTP 200. The error is in the response body's error field. The API returns non-200 status codes only for API-level errors (auth, validation, rate limits).