API Monitoring with Checkly: HTTP Checks, Assertions, and Alerts
Checkly's API checks are HTTP monitors with assertions. You send a request, define what "passing" looks like, and Checkly runs that check from global locations on a schedule. When an assertion fails, you get alerted. Simple premise, a lot of depth in the details.
This guide goes through everything: configuring checks, writing assertions, handling authentication, chaining multi-step checks, and managing environments.
API Check Anatomy
An API check consists of:
- Request — method, URL, headers, body
- Setup script (optional) — JavaScript that runs before the request
- Assertions — what the response must satisfy to pass
- Teardown script (optional) — JavaScript that runs after the response
Most checks only need 1 and 3. The scripts unlock advanced patterns like dynamic auth tokens.
Request Configuration
Basic GET
The simplest check:
- Method:
GET - URL:
https://api.myapp.com/health - Assertion: Status code equals
200
This verifies your health endpoint is reachable and returns success. It takes about 5 minutes to set up and catches network-level failures, application crashes, and misrouted traffic.
POST with a JSON Body
For checks that need to create or modify data:
- Method:
POST - URL:
https://api.myapp.com/v1/items - Headers:
Content-Type: application/json - Body:
{
"name": "Monitor Test Item",
"price": 0.01,
"sku": "MONITOR-TEST-001"
}Add assertions on status code (201) and the response body (the returned item has an id field).
Headers
Add headers in the Headers section of the check form. Static values go directly in the form. For secrets like API keys, reference environment variables:
| Header | Value |
|---|---|
Authorization |
Bearer {{API_KEY}} |
X-Api-Version |
2024-01 |
Content-Type |
application/json |
The {{VARIABLE_NAME}} syntax references Checkly environment variables. The variable's value is injected at runtime and never appears in the UI.
Assertions
Assertions are where Checkly's API monitoring gets specific. You're not just checking "did the request succeed" — you're defining exactly what a successful response looks like.
Status Code
The most basic assertion. Always add this:
| Property | Comparison | Value |
|---|---|---|
| Status code | Equals | 200 |
For POST endpoints that create resources, assert 201. For DELETE, assert 204 or 200 depending on your API conventions. Don't skip this — it's the most common failure mode.
Response Time
Response time | Less than | 2000This asserts the full request completes in under 2 seconds. Set the threshold based on your SLA, not an arbitrary number. If you've committed to p95 under 500ms, assert 500. If you haven't committed to anything, 2000 is a reasonable starting point.
Response time assertions catch degraded performance before users notice slowdowns. A check that passes with a 1900ms response when it usually takes 80ms is a signal worth alerting on.
JSON Body Assertions
This is where API monitoring gets powerful. You can assert on specific fields in the response JSON.
Suppose your GET /api/status returns:
{
"status": "ok",
"database": "connected",
"version": "2.4.1"
}Add these assertions:
| Property | Comparison | Value |
|---|---|---|
json body $.status |
Equals | ok |
json body $.database |
Equals | connected |
Now the check passes only if the API is reachable, returns 200, and the JSON confirms both status and database connectivity. An endpoint that returns 200 with {"status": "degraded"} will correctly fail.
For arrays, you can assert on length:
json body $.items.length | Greater than | 0For nested paths:
json body $.data.user.id | Is not null |Response Headers
Assert on specific response headers:
| Property | Comparison | Value |
|---|---|---|
Header content-type |
Contains | application/json |
Header x-ratelimit-remaining |
Greater than | 0 |
The rate limit assertion is particularly useful — it tells you if your monitoring itself is consuming your API quota.
Authentication
Static API Keys
Store your API key as a Checkly environment variable (mark as secret), then reference it in the request header:
Authorization: Bearer {{API_KEY}}At the account level: Account Settings → Environment Variables → + Add Variable.
For production vs staging keys, use Group-level variables. Create a group for production checks, another for staging checks, each with their own API_KEY variable pointing to the appropriate key.
Dynamic Tokens (OAuth, JWT)
When your API requires a fresh JWT or OAuth token, use a Setup Script to fetch the token before the main request runs.
In the check's Setup tab:
// Fetch a fresh auth token before each check run
const response = await fetch('https://auth.myapp.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
});
const data = await response.json();
// Make the token available to the request via an environment variable
process.env.ACCESS_TOKEN = data.access_token;Then in your request headers:
Authorization: Bearer {{ACCESS_TOKEN}}The setup script runs before every check execution. The token is fresh for every run. This handles token expiry correctly — no stale credential failures.
Basic Auth
For APIs using HTTP Basic Auth, either:
- Include credentials in the URL:
https://user:password@api.myapp.com/endpoint - Add an
Authorizationheader with a base64-encoded value:Basic {{BASE64_CREDENTIALS}}
Option 2 is cleaner — store the base64-encoded credentials as a secret variable.
Multi-Step API Checks
Sometimes you need to make multiple requests in sequence — create a resource, then fetch it, then delete it. This is where Setup Scripts and Teardown Scripts combine.
However, Checkly's API checks don't natively chain multiple HTTP requests in the UI. For multi-step scenarios, you have two options:
Option 1: Browser check with Playwright
Use Playwright's request context for pure HTTP chaining without a browser:
const { chromium } = require('playwright');
const request = await (await chromium.launch()).newContext().request;
// Step 1: Create an item
const createResponse = await request.post('https://api.myapp.com/items', {
data: { name: 'Test Item', price: 9.99 }
});
const item = await createResponse.json();
console.log('Created item:', item.id);
// Step 2: Fetch the created item
const getResponse = await request.get(`https://api.myapp.com/items/${item.id}`);
if (!getResponse.ok()) {
throw new Error(`Failed to fetch item ${item.id}: ${getResponse.status()}`);
}
// Step 3: Delete the item (cleanup)
await request.delete(`https://api.myapp.com/items/${item.id}`);
console.log('Cleanup complete');Option 2: Setup Script for the first call
Use the setup script to make the first request and extract what you need:
// Setup: Create a temporary resource and store its ID
const response = await fetch('https://api.myapp.com/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.API_KEY}` },
body: JSON.stringify({ userId: 'test-user-123' })
});
const session = await response.json();
process.env.SESSION_ID = session.id;Then the main request hits /sessions/{{SESSION_ID}} to verify the session is retrievable, and a teardown script deletes it.
Environment Management
Account-Level Variables
Shared across all checks. Use for:
- API keys that apply everywhere
- Common test data (shared user IDs, product SKUs)
- Timeouts and constants
Group-Level Variables
Override account-level variables within a group. Use for:
BASE_URL(production vs staging)- Environment-specific API keys
- Feature flags per environment
Create two groups: Production Checks and Staging Checks. Give each a BASE_URL variable. Your check URLs become {{BASE_URL}}/api/products. When you add a new check, put it in the right group and it automatically targets the right environment.
Local Overrides for Testing
When running checks locally via the Checkly CLI, you can override environment variables:
npx checkly test --env-file .env.localThis lets you test against localhost during development without changing the live configuration.
Alert Configuration for API Checks
For production API checks, configure alert channels with:
Alert on failure: immediately (no threshold delay for critical endpoints)
Alert on degraded: when response time exceeds your threshold
Alert on recovery: always — you need to know when incidents resolve
For high-traffic endpoints where brief blips are normal, add a retry (one retry from a different location before alerting). This eliminates false positives from transient network issues without meaningfully increasing mean time to alert.
Check Frequency Guidelines
| Endpoint Type | Recommended Frequency |
|---|---|
| Health / status endpoint | 1 minute |
| Authentication (login, token) | 5 minutes |
| Core business flows (checkout, create order) | 5 minutes |
| Secondary endpoints (user profile, search) | 10 minutes |
| Background or batch endpoints | 15-30 minutes |
Don't set everything to 1 minute. More frequency = more check runs = higher cost, and for most endpoints a 5-minute detection window is acceptable.
Debugging Failed Checks
When a check fails, Checkly shows you:
- Which location(s) failed — regional vs global outage
- Full request details — exactly what was sent
- Full response — status code, headers, body (or error if no response)
- Which assertion failed — with the expected vs actual value
Common failure patterns:
- All locations fail simultaneously → your API is down or returning errors globally
- One location fails, others pass → DNS, routing, or CDN issue in that region
- Check passes immediately but fails on retry → race condition or intermittent error
- Response time assertion fails → performance degradation, investigate slow queries or resource contention
- JSON body assertion fails → API returned 200 but with unexpected data — a logic bug, not a crash
The most useful debugging step is always: look at the actual response body. If your API is returning a 200 with an error message embedded in JSON (an unfortunately common pattern), the response body tells you that immediately.