Public API
Everything you can do in the Canary9 dashboard is also available over a plain REST API: create and manage endpoints, alert policies, integrations, and team members, and pull uptime and check-result data into your own tools. It's the same functionality the dashboard uses, scoped to your organization by an API key you control.
Overview
- Base URL:
https://api.canary9.com/v1 - Format: every request and response body is JSON. Send
Content-Type: application/jsonon requests with a body. - Authentication: every request carries an API key in the
Authorizationheader. - Errors: every error response is a JSON object shaped
{"detail": "..."}, with a standard HTTP status code. - Interactive reference: the interactive API explorer lives at https://api.canary9.com/docs, where you can authorize with your API key and try requests straight from the browser.
curl https://api.canary9.com/v1/endpoints/list \
-H "Authorization: Bearer cnry_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" API keys
API keys are managed from Organization → API keys. Creating, renaming, rotating, and revoking keys is an admin-only action, and API access itself requires the Pro plan or higher. A Free-plan organization sees an upgrade prompt instead of the create form.
Creating a key
Give the key a name, choose its scopes, and optionally set an expiry date and a custom rate limit. Canary9 shows the full raw key (starting with cnry_) exactly once, immediately after creation.
Scopes
Every key is granted one or more scopes, each covering read or write access to one resource family. GET/HEAD requests need the matching :read scope; every other method needs :write.
endpoints:read
List and read monitored endpoints, check results, uptime, and monitoring locations.
endpoints:write
Create, update, and delete monitored endpoints.
alerts:read
List and read alert policies and the alert event log.
alerts:write
Create and update alert policies, mute or test them, and record alert events.
integrations:read
List and read notification integrations (secrets are always masked).
integrations:write
Create, update, delete, and test notification integrations.
organizations:read
Read your organization's details, for data lookups in tools like Terraform. There is deliberately no write counterpart: organization changes stay in the dashboard.
users:read
List your organization's members and pending invites.
users:write
Invite, update, and remove organization members.
An API key can never manage other API keys, billing, or organization settings (organization details are readable with organizations:read, but all changes stay in the dashboard): management of those stays session-only, so a leaked key can't be used to escalate itself. Requests to those areas return 403.
Rate limits
Each key is limited to 300 requests per minute by default; an admin can set a different limit for a specific key at creation. The limit is shared across all of Canary9's API servers, so it is the same ceiling no matter which server answers. Every successful response carries your current allowance in X-RateLimit-Limit and X-RateLimit-Remaining, so a client can pace itself before it is ever blocked. Exceeding the limit returns 429 with the retry window and your current quota in the response headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
Content-Type: application/json
{"detail": "Rate limit exceeded"} Canary9's edge also applies a per-address safety cap well above the per-key limit (1,200 requests per minute per source IP). Tripping it returns the same 429 and {"detail": "Rate limit exceeded"} body with Retry-After: 60, so a client that backs off on 429 handles both limits the same way. If you run many keys behind one address and expect to approach that cap, contact support@canary9.com.
Rotating and revoking a key
- Rename: update a key's label without affecting its scopes or access.
- Rotate: issues a brand-new key with the same name, scopes, rate limit, and expiry, and immediately revokes the old one. The new raw key is shown once, exactly like at creation, so update anywhere the old key was used.
- Revoke: immediately and permanently disables a key. Any request made with it afterwards is rejected.
Endpoints
Manage the checks Canary9 runs against your sites, APIs, and infrastructure. See Monitoring endpoints for what each field means in the dashboard.
List endpoints
curl https://api.canary9.com/v1/endpoints/list \
-H "Authorization: Bearer $CANARY9_API_KEY" {
"data": [
{
"id": "3fa2b6c0-...-e6e1",
"organization_id": "org_...",
"name": "Marketing site",
"target": "https://canary9.com",
"endpoint_type": "http",
"interval_seconds": 60,
"enabled": true,
"labels": [{ "label": "prod", "color": "green" }],
"dns_record_type": "A",
"regions": ["us-east-2.aws"],
"content_match_type": "none",
"content_match": null,
"content_match_negate": false,
"created_at": "2026-08-01T12:00:00+00:00",
"updated_at": "2026-08-01T12:00:00+00:00",
"stale_regions": [],
"regions_blocked_reason": null
}
],
"total": 1
} Create an endpoint
curl -X POST https://api.canary9.com/v1/endpoints/create \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Marketing site",
"target": "https://canary9.com",
"endpoint_type": "http",
"interval_seconds": 60,
"regions": ["us-east-2.aws", "eu-central-1.aws"]
}' name: required.target: required. A URL forhttp/browser; a hostname or IP forping/dns; a hostname or IP, optionally with:port, fortcp.endpoint_type:http,tcp,ping,dns, orbrowser. Defaults tohttp.interval_seconds: one of30,60,300,600,3600. Defaults to60. Your plan sets the fastest value you can use (every 5 minutes on Free, 1 minute on Startup and Pro, 30 seconds on Enterprise); anything faster returns422naming the fastest interval available to you.enabled: defaults totrue. Setfalseto save a draft that doesn't run and doesn't count against your plan until you enable it.labels: up to 20{"label": "...", "color": "..."}pairs. Labels are alphanumeric, up to 50 characters;coloris one ofred,green,yellow,blue,orange,purple,pink,teal,gray,white.dns_record_type: fordnsendpoints:A,AAAA,TXT,MX,CNAME,NS,PTR,CAA, orSRV. Defaults toA.regions: region ids to check from (see monitoring locations). Omit it to let Canary9 assign a default automatically.content_match_type,content_match,content_match_negate: HTTP-only content assertion.content_match_typeisnone(default),keyword, orregex;content_match(required unlessnone, max 500 characters) is the value to look for;content_match_negate(defaultfalse) inverts the check to "must not contain".
Enabling more checks than your plan allows returns 422 with a detail explaining the limit; see Plans & billing.
Get, update, and delete an endpoint
curl https://api.canary9.com/v1/endpoints/{id} \
-H "Authorization: Bearer $CANARY9_API_KEY"
curl -X PATCH https://api.canary9.com/v1/endpoints/{id} \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{"interval_seconds": 30, "enabled": false}'
curl -X DELETE https://api.canary9.com/v1/endpoints/{id} \
-H "Authorization: Bearer $CANARY9_API_KEY" PATCH accepts any subset of the fields above and only changes what you send. DELETE returns 204 with an empty body.
Check results
Recent check results for one endpoint, newest first.
curl "https://api.canary9.com/v1/endpoints/{id}/results?limit=90" \
-H "Authorization: Bearer $CANARY9_API_KEY" limit defaults to 90 (up to a year of one-minute checks); an optional since (ISO 8601 timestamp) returns only results at or after that time. Each result includes success, status_code, latency_ms, error, checked_at, dns_records, has_screenshot, body_snippet, TLS certificate expiry, per-phase timing (phase_timings, failed_phase), and block_reason when a check was blocked by bot management rather than failed.
Uptime
curl "https://api.canary9.com/v1/endpoints/{id}/uptime?window=24h" \
-H "Authorization: Bearer $CANARY9_API_KEY" {
"endpoint_id": "3fa2b6c0-...-e6e1",
"window": "24h",
"since": "2026-08-15T12:00:00+00:00",
"until": "2026-08-16T12:00:00+00:00",
"up_checks": 1438,
"total_checks": 1440,
"uptime_pct": 99.86,
"status": "ok"
} window is one of 1h, 3h, 6h, 12h, 24h, 7d, or 30d (default 24h). status is ok once there's a real figure, collecting for an endpoint too new to score yet, or no_data when the window has no scoreable checks (uptime_pct is null in both of those cases).
Monitoring locations
curl https://api.canary9.com/v1/regions/list \
-H "Authorization: Bearer $CANARY9_API_KEY" Returns the probe locations available to select in regions when creating or updating an endpoint, plus max_selectable, the most you can assign to one endpoint.
Alert policies
An alert policy decides what counts as a failure and routes the notification. See Alerts & notifications for the full concept: conditions by check type, warning/critical levels, and the default managed policies.
List, get, create, update, and delete
curl https://api.canary9.com/v1/alerts/policies/ \
-H "Authorization: Bearer $CANARY9_API_KEY"
curl -X POST https://api.canary9.com/v1/alerts/policies/ \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Prod API down",
"endpoint_ids": ["3fa2b6c0-...-e6e1"],
"success_criteria_type": "status_range",
"success_status_min": 500,
"success_status_max": 599,
"consecutive_failures": 3,
"recovery_successes": 2
}' Key fields on PolicyCreate/PolicyUpdate:
name: required, 1–200 characters.endpoint_ids: endpoint ids the policy watches. Omit it (or pass an empty list) to apply to every endpoint of the matching type.input_type:http,ping,dns,tcp, orany. Inferred fromsuccess_criteria_typeif omitted.success_criteria_type:status_code,status_range,dns_no_resolve,response_time_ms,tcp_port_closed,certificate_expiry_days, ormissing_data. Must be one this policy'sinput_typesupports; see Conditions by check type.expected_status_code/warning_status_code: forstatus_code.success_status_min/success_status_max,warning_status_min/warning_status_max: forstatus_range.response_time_threshold_ms/warning_response_time_threshold_ms: forresponse_time_ms. The warning threshold, if set, must be lower than the critical one.certificate_expiry_threshold_days: forcertificate_expiry_days. Defaults to 14.consecutive_failures/recovery_successes: how many consecutive bad/good checks flip the policy. Default3/2.region_scope:any(default) orall, controlling how a multi-region endpoint's per-region results combine.muted: suppress notifications without deleting the policy.message: optional notification message template.
Canary9's seven managed default policies are immutable: PATCH on one only accepts {"muted": true|false} (any other field returns 403), and DELETE on one returns 403; mute it instead.
Set the notification message
curl -X PATCH https://api.canary9.com/v1/alerts/policies/{id}/notification \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "{{ #critical }}API is down @pagerduty-oncall{{ #end_critical }}"}' Unlike the general PATCH, this route is allowed on default policies too, since it only ever touches message.
Test a policy
curl -X POST https://api.canary9.com/v1/alerts/policies/{id}/test \
-H "Authorization: Bearer $CANARY9_API_KEY" Sends a [TEST]-prefixed notification for each transition condition (critical, warning, critical recovery, warning recovery, missing data) through the policy's configured integrations, and returns how many notifications were dispatched.
Alert events
The event log Canary9 writes to whenever a policy's condition trips or clears.
curl "https://api.canary9.com/v1/alerts/events/?policy_id={id}" \
-H "Authorization: Bearer $CANARY9_API_KEY"
curl https://api.canary9.com/v1/alerts/events/{event_id} \
-H "Authorization: Bearer $CANARY9_API_KEY" Each event has event_type (down, up, degraded, or no_data), severity (critical, warning, or info), message, created_at, the owning alert_policy_id, and, for down events on web, browser, TCP, and ping checks, an attached diagnostics snapshot (see the event log).
Canary9's own monitoring pipeline records events automatically as checks change state; you can also record one directly, which fans a notification out through the policy's integrations immediately:
curl -X POST https://api.canary9.com/v1/alerts/events/ \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alert_policy_id": "...",
"event_type": "down",
"severity": "critical",
"message": "Manually escalated by on-call"
}' Integrations
Notification channels: Slack, Microsoft Teams, Discord, PagerDuty, Jira, webhooks, and email. See Integrations for the dashboard setup walkthrough for each type, and how @handle routing works in a policy's notification message.
List, get, create, update, delete, and test
curl https://api.canary9.com/v1/integrations/list \
-H "Authorization: Bearer $CANARY9_API_KEY"
curl -X POST https://api.canary9.com/v1/integrations/create \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "slack",
"name": "Web team",
"config": {"webhook_url": "https://hooks.slack.com/services/..."}
}'
curl -X POST https://api.canary9.com/v1/integrations/{id}/test \
-H "Authorization: Bearer $CANARY9_API_KEY" type is one of slack, pagerduty, jira, webhook, teams, discord, or email. handle (the @mention slug used to route a policy message) defaults to a slugified version of name when omitted, and must be unique within your organization. config is type-specific:
slack:webhook_url(Slack Incoming Webhook URL); optionalchannel,username,emoji.teams:webhook_url(Microsoft Teams Workflows webhook URL).discord:webhook_url(Discord Incoming Webhook URL); optionalusername,avatar_url.pagerduty:routing_key; optionalauto_resolve(defaulttrue) to resolve the correlated incident when the check recovers.jira:base_url,email,api_token,project_key; optionalissue_type(defaultTask),auto_resolve(defaulttrue),resolve_transition(defaultDone), andextra_fields(an object merged into every created issue'sfields, for project-specific required fields).webhook:url; optionalmethod(defaultPOST),headers, andpayload_template(a JSON string with{{event_type}},{{severity}},{{message}},{{created_at}},{{dedup_key}}tokens).email:recipients(1–10 addresses); optionalsubject_prefix.
Responses always mask secret-bearing config fields (webhook URLs, API tokens, routing keys) to a short, non-reversible hint; the full value is never returned after creation. PATCH with config replaces the whole object; sending back a masked value unchanged for a field you didn't mean to touch leaves the stored secret as-is rather than overwriting it with the mask.
Organization
Read-only organization details, useful as a data source in infrastructure-as-code tools.
curl https://api.canary9.com/v1/organizations/current \
-H "Authorization: Bearer $CANARY9_API_KEY" { "id": "3fa2b6c0-...-e6e1", "name": "Acme Corp" } Requires the organizations:read scope. Write requests to this area always return 403 for API keys.
Users
Manage the people in your organization: list members, invite new teammates by email, and update or remove existing ones. See Account & team for the same actions from the dashboard.
List users
curl https://api.canary9.com/v1/users/list \
-H "Authorization: Bearer $CANARY9_API_KEY" {
"data": [
{
"id": "3fa2b6c0-...-e6e1",
"email": "jamie@acme.com",
"name": "Jamie Rivera",
"role": "admin",
"active": true,
"email_verified": true
}
],
"total": 1
} Invite a user
curl -X POST https://api.canary9.com/v1/users/invite \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new.teammate@acme.com",
"name": "Taylor Chen",
"role": "member"
}' {
"id": "7c1e4a9b2f6d8031...4e2a",
"email": "new.teammate@acme.com",
"name": "Taylor Chen",
"role": "member",
"invited_by": "apikey:cnry_a1b2c3d4",
"created_at": "2026-08-17T12:00:00+00:00",
"expires_at": "2026-08-24T12:00:00+00:00",
"accepted_at": null
} email: required.name: required, 1–100 characters.role:memberoradmin.
invited_by is either a user id (invites sent from the dashboard) or apikey:<prefix> (invites sent with an API key, as above). Canary9 emails the invite link right away, and the raw link is never returned in this response, so the invite only ever exists in the recipient's inbox. Returns 409 if the email already belongs to a registered user, or if there's already a pending, unexpired invite for it.
List pending invites
curl https://api.canary9.com/v1/users/invites \
-H "Authorization: Bearer $CANARY9_API_KEY" {
"data": [
{
"id": "7c1e4a9b2f6d8031...4e2a",
"email": "new.teammate@acme.com",
"name": "Taylor Chen",
"role": "member",
"invited_by": "apikey:cnry_a1b2c3d4",
"created_at": "2026-08-17T12:00:00+00:00",
"expires_at": "2026-08-24T12:00:00+00:00",
"accepted_at": null
}
],
"total": 1
} Only invites that haven't been accepted or expired show up here.
Revoke a pending invite
curl -X DELETE https://api.canary9.com/v1/users/invites/{invite_id} \
-H "Authorization: Bearer $CANARY9_API_KEY" Cancels a pending invite before it's accepted, so a mistaken or unwanted invitation can be pulled back rather than left to expire on its own. Requires users:write. Returns 204 on success, or 404 if the invite doesn't exist in your organization or has already been accepted.
Accept an invite
This is a browser flow, not something you'd script against: the invite email links a new teammate to https://app.canary9.com/accept-invite?token=..., where they set their password. It's documented here for completeness, and it's the one route in this API that works without an API key or session at all, since the invite token itself is the credential. An API key can still call it, but only if it also carries users:write.
curl -X POST https://api.canary9.com/v1/users/invite/accept \
-H "Content-Type: application/json" \
-d '{
"token": "the-token-from-the-invite-link",
"password": "a-strong-password"
}' { "ok": true } token: required, the token from the invite link.password: required, at least 8 characters.name: optional, overrides the name the inviter set.
Returns 400 with {"detail": "Invite is invalid or has expired"} for an unknown, expired, or already-accepted token, the same message in every case, so it can't be used to probe which invites exist. There's no auto-login: accepting sends the new teammate on to sign in with their new password.
Update or remove a user
curl -X PATCH https://api.canary9.com/v1/users/{user_id} \
-H "Authorization: Bearer $CANARY9_API_KEY" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
curl -X DELETE https://api.canary9.com/v1/users/{user_id} \
-H "Authorization: Bearer $CANARY9_API_KEY" {
"id": "3fa2b6c0-...-e6e1",
"email": "jamie@acme.com",
"name": "Jamie Rivera",
"role": "admin",
"active": true,
"email_verified": true
} PATCH accepts any subset of name (1–100 characters), role (member or admin), and active, and only changes what you send. DELETE returns 204 with an empty body.
Every organization must always keep at least one active admin. Demoting, deactivating, or deleting the last one returns 409 with {"detail": "An organization must keep at least one active admin."} instead, promote or activate another admin first, then retry.
Errors
Every error is {"detail": "<human-readable message>"} with a standard status code: 401 for an authentication problem, 403 for a permission or plan problem, 404 when the resource doesn't exist (or belongs to another organization), 422 for a validation or plan-limit problem, 429 for rate limiting, and 502 when Canary9 couldn't reach a downstream service (e.g. a test notification's destination).
Troubleshooting
Start from the status code you're seeing.
401 Unauthorized
The key is missing, malformed, revoked, or past its expires_at. Check that the Authorization: Bearer cnry_... header is present and well-formed, and that the key still shows as active under Organization → API keys.
403 Forbidden
Either the key is missing the scope the route needs (create a new key with the right scope, since scopes can't be changed after creation), or your organization's plan doesn't include API access (upgrade from Organization → Billing). It's also returned for routes a key can never reach at all, like managing other keys or billing.
429 Too Many Requests
You've exceeded the key's rate limit. Back off until the Retry-After header's window has passed, or ask an admin to raise the key's limit.
Still need help?
If these steps didn't resolve your issue, reach out to support@canary9.com. Include the endpoint or affected request, and a request id or timestamp if you have one, and we'll help you get to the bottom of it.