Flow Monitors
Create and manage flow monitors. Run a multi-request spec on a schedule and read what each run asserted.
A flow monitor asks a question one HTTP check cannot: does a sequence still work? It holds a spec — several HTTP requests with assertions between them — and runs it on a schedule. A run makes HTTP requests, parses URLs, and reads JWTs. Nothing else: there is no browser, no page to render, no script of yours to execute.
Every response on this resource carries the standard envelope — data alongside meta, which holds the requestId to quote in a support request and the timestamp the response was written. The schema blocks below describe data only.
The spec is never returned
source is write-only. You send it on create and update; no endpoint gives it back. A sign-in flow writes a real password into its spec, so a key that may list monitors is not thereby a key that may read the credentials they sign in with — the same reading this API already applies to an alert's webhook URL and secret.
Read a monitor's spec in the dashboard, where a signed-in member is standing in front of it. To keep a copy alongside your own configuration, keep the text you sent.
What a flow may reach
A flow only drives a domain your team has verified. The hosts a spec names are resolved against the team's verified domains on every write and again on every run: verifying example.com covers app.example.com, but nothing at example.net. Two things follow, and both are refusals rather than warnings:
A spec naming a host no verified domain covers is refused, naming the host.
A spec naming no host at all is refused too. Every URL a flow requests has to be written into the spec, because a URL assembled at run time cannot be checked against anything.
The check runs on POST, on a PATCH that changes source, and on a PUT that carries one, so a monitor cannot be edited onto a host it could not have been created on. Un-verifying a domain closes this endpoint to it on the very next call.
Intervals are a fixed list
A flow run costs orders of magnitude more than a single ping, so intervalSeconds is one of seven priced values rather than any number in a range:
900, 1800, 3600, 10800, 21600, 43200, 86400
Anything else is refused. Nothing is rounded to the nearest allowed value — a monitor you believed ran every minute and actually ran hourly is a worse outcome than a 400.
Scopes
Flow monitors are read with flow-monitors:read, written with flow-monitors:write and deleted with flow-monitors:delete.
The flow monitor object
{
"id": "flow_abc123",
"name": "Sign in and read profile",
"intervalSeconds": 3600,
"isEnabled": true,
"lastCheckedAt": 1787000000000,
"lastStatus": "up",
"createdAt": 1786300000000,
"updatedAt": 1786400000000
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "string", "description": "Unique identifier for the flow monitor" },
"name": {
"type": "string",
"minLength": 1,
"maxLength": 255,
"description": "Human-readable name for the monitor"
},
"intervalSeconds": {
"type": "integer",
"enum": [900, 1800, 3600, 10800, 21600, 43200, 86400],
"default": 3600,
"description": "How often the flow runs"
},
"isEnabled": {
"type": "boolean",
"default": true,
"description": "Whether the flow runs on its interval"
},
"lastCheckedAt": {
"type": ["integer", "null"],
"description": "When the flow last ran, in milliseconds since the epoch"
},
"lastStatus": {
"type": ["string", "null"],
"enum": ["up", "down", "error", null],
"description": "What the last run concluded. `up` means every assertion held; `down` means one failed, so the flow is broken; `error` means the run could not find out — a spec that will not parse, or a host outside the team's verified domains. Null until the first run."
},
"createdAt": { "type": "integer", "description": "When the monitor was created" },
"updatedAt": { "type": "integer", "description": "When the monitor was last updated" }
},
"required": [
"id",
"name",
"intervalSeconds",
"isEnabled",
"lastCheckedAt",
"lastStatus",
"createdAt",
"updatedAt"
]
}source is absent from this object by design. See The spec is never returned.
List All Flow Monitors
/api/v1/flow-monitorsRetrieves your team's flow monitors, most recently created first.
This endpoint is paginated. See Pagination for how to page through the full list.
Required scope: flow-monitors:read
Query Parameters
perPageintegerResults per page, 1-200 (default: 50)
cursorstringPage to fetch, taken from a
Linkheader
Errors (3)
| Status | Type | Meaning |
|---|---|---|
| 400 | bad-request | The request is malformed |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
Response fields (200)
dataobjectRequiredShow nested fields
flowMonitorsFlowMonitor[]RequiredShow nested fields
idstringRequirednamestringRequiredintervalSecondsintegerRequiredOne of
900,1800,3600,10800,21600,43200,86400.isEnabledbooleanRequiredlastCheckedAtinteger | nullRequiredEpoch milliseconds
lastStatusstring | nullRequiredOne of
up,down,error.createdAtintegerRequiredEpoch milliseconds
updatedAtintegerRequiredEpoch milliseconds
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
paginationobjectRequiredShow nested fields
nextstring | nullRequiredCursor of the following page
prevstring | nullRequiredCursor of the preceding page
perPageintegerRequiredtotalinteger
curl -i "https://uptime.sergiodxa.com/api/v1/flow-monitors?perPage=25" \
-H "Authorization: Bearer uptime_your_api_key"Response
{
"data": {
"flowMonitors": [
{
"id": "flow_abc123",
"name": "Sign in and read profile",
"intervalSeconds": 3600,
"isEnabled": true,
"lastCheckedAt": 1787000000000,
"lastStatus": "up",
"createdAt": 1786300000000,
"updatedAt": 1786400000000
}
]
},
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z",
"pagination": {
"next": "eyJkIjoiYWZ0ZXIi",
"prev": null,
"perPage": 25,
"total": 29
}
}
}Create a Flow Monitor
/api/v1/flow-monitorsCreates a flow monitor. The spec is checked before anything is stored: it has to parse, and every host it names has to be covered by one of the team's verified domains. A refused create leaves no row behind.
A new monitor is scheduled for its first run on the next cron tick, so it reports a status straight away rather than after one silent interval.
The response is 201 Created.
Required scope: flow-monitors:write
Request Body
namestringRequiredMonitor name (1-255 characters)
sourcestringRequiredThe spec text, up to 20000 characters. Stored, never returned.
intervalSecondsintegerOne of 900, 1800, 3600, 10800, 21600, 43200, 86400 (default: 3600)
isEnabledbooleanWhether the flow runs on its interval (default: true)
The spec you just sent is not echoed back.
Rejected: an interval that is not on the list
curl https://uptime.sergiodxa.com/api/v1/flow-monitors \
-H "Authorization: Bearer uptime_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Every minute", "source": "...", "intervalSeconds": 60 }'{
"type": "https://uptime.sergiodxa.com/docs/api/errors/validation-error",
"title": "The request failed validation",
"status": 400,
"instance": "urn:uuid:0b6a4c1e-3f7d-4e8a-9c21-5d8f0a7b3e64",
"errors": [
{
"pointer": "/intervalSeconds",
"code": "invalid",
"message": "Expected one of: 900, 1800, 3600, 10800, 21600, 43200, 86400"
}
]
}Rejected: a host no verified domain covers
curl https://uptime.sergiodxa.com/api/v1/flow-monitors \
-H "Authorization: Bearer uptime_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Somebody else s site",
"source": "use http\ntest \"reaches elsewhere\" {\n\twhen {\n\t\tlet response = http.get \"https://victim.example.net/login\"\n\t}\n}"
}'{
"type": "https://uptime.sergiodxa.com/docs/api/errors/validation-error",
"title": "The request failed validation",
"status": 400,
"detail": "This flow reaches victim.example.net, which no verified domain on this team covers. A flow monitor can only drive a domain the team has verified.",
"instance": "urn:uuid:0b6a4c1e-3f7d-4e8a-9c21-5d8f0a7b3e64",
"errors": [
{
"pointer": "/source",
"code": "invalid",
"message": "This flow reaches victim.example.net, which no verified domain on this team covers. A flow monitor can only drive a domain the team has verified."
}
]
}A spec that names no host at all is refused the same way, with:
{
"type": "https://uptime.sergiodxa.com/docs/api/errors/validation-error",
"title": "The request failed validation",
"status": 400,
"detail": "This flow names no host to reach. Every URL it requests has to be written in the spec, so it can be checked against the team's verified domains.",
"instance": "urn:uuid:0b6a4c1e-3f7d-4e8a-9c21-5d8f0a7b3e64",
"errors": [
{
"pointer": "/source",
"code": "invalid",
"message": "This flow names no host to reach. Every URL it requests has to be written in the spec, so it can be checked against the team's verified domains."
}
]
}Errors (6)
| Status | Type | Meaning |
|---|---|---|
| 400 | validation-error | The request failed validation |
| 400 | idempotency-key-invalid | The Idempotency-Key header is not valid |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
| 409 | idempotency-key-in-use | A request with this idempotency key is still being processed |
| 422 | idempotency-key-reused | This idempotency key was already used for a different request |
Response fields (201)
dataobjectRequiredShow nested fields
flowMonitorobjectRequiredShow nested fields
idstringRequirednamestringRequiredintervalSecondsintegerRequiredOne of
900,1800,3600,10800,21600,43200,86400.isEnabledbooleanRequiredlastCheckedAtinteger | nullRequiredEpoch milliseconds
lastStatusstring | nullRequiredOne of
up,down,error.createdAtintegerRequiredEpoch milliseconds
updatedAtintegerRequiredEpoch milliseconds
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
Response
{
"data": {
"flowMonitor": {
"id": "flow_abc123",
"name": "Sign in and read profile",
"intervalSeconds": 3600,
"isEnabled": true,
"lastCheckedAt": null,
"lastStatus": null,
"createdAt": 1786400000000,
"updatedAt": 1786400000000
}
},
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z"
}
}Get a Flow Monitor
/api/v1/flow-monitors/:flowMonitorIdRetrieves a single flow monitor by ID.
Required scope: flow-monitors:read
A monitor belonging to another team is 404, not 403: an id you may not read is an id that does not exist.
The detail response carries no more than the list response does: source is withheld from both, so neither is the place to read a spec back.
Errors (4)
| Status | Type | Meaning |
|---|---|---|
| 400 | validation-error | The request failed validation |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
| 404 | not-found | The resource does not exist |
Response fields (200)
dataobjectRequiredShow nested fields
flowMonitorobjectRequiredShow nested fields
idstringRequirednamestringRequiredintervalSecondsintegerRequiredOne of
900,1800,3600,10800,21600,43200,86400.isEnabledbooleanRequiredlastCheckedAtinteger | nullRequiredEpoch milliseconds
lastStatusstring | nullRequiredOne of
up,down,error.createdAtintegerRequiredEpoch milliseconds
updatedAtintegerRequiredEpoch milliseconds
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
Response
{
"data": {
"flowMonitor": {
"id": "flow_abc123",
"name": "Sign in and read profile",
"intervalSeconds": 3600,
"isEnabled": true,
"lastCheckedAt": 1787000000000,
"lastStatus": "up",
"createdAt": 1786300000000,
"updatedAt": 1786400000000
}
},
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z"
}
}Update a Flow Monitor
/api/v1/flow-monitors/:flowMonitorIdUpdates a flow monitor with a JSON merge patch. Every field is optional; only the ones you send change.
A new source replaces the spec, and the replacement goes through the same verified-domain check a create does. Changing intervalSeconds or isEnabled reschedules the monitor in the same write, so the next run honours the new setting rather than the old one.
Required scope: flow-monitors:write
Request Body
The fields and limits are Create a Flow Monitor's. null resets intervalSeconds and isEnabled to their defaults; name and source cannot be removed.
namestringMonitor name (1-255 characters)
sourcestringReplacement spec text, up to 20000 characters
intervalSecondsintegerOne of 900, 1800, 3600, 10800, 21600, 43200, 86400
isEnabledbooleanWhether the flow runs on its interval
A refused update changes nothing: the stored spec, interval and schedule are exactly what they were before the call.
Update With PUT
PUT /api/v1/flow-monitors/:flowMonitorId takes the same fields, for integrations written before PATCH existed. A field you leave out keeps its value, null is refused, a source you send goes through the verified-domain check even when it is unchanged, and isEnabled: true schedules an immediate run on every request.
Required scope: flow-monitors:write
Errors (4)
| Status | Type | Meaning |
|---|---|---|
| 400 | validation-error | The request failed validation |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
| 404 | not-found | The resource does not exist |
Response fields (200)
dataobjectRequiredShow nested fields
flowMonitorobjectRequiredShow nested fields
idstringRequirednamestringRequiredintervalSecondsintegerRequiredOne of
900,1800,3600,10800,21600,43200,86400.isEnabledbooleanRequiredlastCheckedAtinteger | nullRequiredEpoch milliseconds
lastStatusstring | nullRequiredOne of
up,down,error.createdAtintegerRequiredEpoch milliseconds
updatedAtintegerRequiredEpoch milliseconds
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
Response
{
"data": {
"flowMonitor": {
"id": "flow_abc123",
"name": "Sign in and read profile",
"intervalSeconds": 21600,
"isEnabled": false,
"lastCheckedAt": 1787000000000,
"lastStatus": "up",
"createdAt": 1786300000000,
"updatedAt": 1787003600000
}
},
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z"
}
}Delete a Flow Monitor
/api/v1/flow-monitors/:flowMonitorIdPermanently deletes a flow monitor, its spec, and its run history.
Required scope: flow-monitors:delete
Errors (4)
| Status | Type | Meaning |
|---|---|---|
| 400 | validation-error | The request failed validation |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
| 404 | not-found | The resource does not exist |
Response fields (200)
dataobjectRequiredShow nested fields
deletedtrueRequired
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
Response
{
"data": { "deleted": true },
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z"
}
}Get Flow Monitor Results
/api/v1/flow-monitors/:flowMonitorId/resultsRetrieves the run history for a flow monitor: one row per run, however many requests that run made. Newest first.
Runs arrive a page at a time. See Pagination for how to walk the whole history.
Required scope: flow-monitors:read
Query Parameters
perPageintegerResults per page, 1-200 (default: 50)
cursorstringPage to fetch, taken from a
Linkheader
Only the first failure of a run is recorded, in failedTest, failedAtLine and failureDetail. That is the one worth reading during an incident: once a sign-in breaks, everything after it fails for the same reason.
requestsMade is what the run cost. A flow is metered as one ping per request it performed, so a three-request flow on the hourly interval bills like three hourly HTTP monitors.
durationMs is the wall-clock of the whole run, which is also this monitor's latency series.
Errors (5)
| Status | Type | Meaning |
|---|---|---|
| 400 | bad-request | The request is malformed |
| 400 | validation-error | The request failed validation |
| 401 | unauthorized | The API key is missing or invalid |
| 403 | forbidden | The API key lacks the required scope |
| 404 | not-found | The resource does not exist |
Response fields (200)
dataobjectRequiredShow nested fields
resultsFlowMonitorResult[]RequiredShow nested fields
idstringRequiredstatusstringRequiredOne of
up,down,error.testsTotalintegerRequiredtestsPassedintegerRequiredtestsFailedintegerRequiredrequestsMadeintegerRequiredHTTP requests the run made; each bills
failedTeststring | nullRequiredfailedAtLineinteger | nullRequiredfailureDetailstring | nullRequireddurationMsinteger | nullRequirederrorMessagestring | nullRequiredWhy the run could not be performed; set with
erroronlycheckedAtintegerRequiredEpoch milliseconds
metaobjectRequiredShow nested fields
requestIdstringRequiredFormat: uuid.
timestampstringRequiredFormat: date-time.
paginationobjectRequiredShow nested fields
nextstring | nullRequiredCursor of the following page
prevstring | nullRequiredCursor of the preceding page
perPageintegerRequiredtotalinteger
curl -i "https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123/results?perPage=10" \
-H "Authorization: Bearer uptime_your_api_key"Response
{
"data": {
"results": [
{
"id": "flowres_xyz789",
"status": "down",
"testsTotal": 2,
"testsPassed": 1,
"testsFailed": 1,
"requestsMade": 2,
"failedTest": "a member can sign in and read their profile",
"failedAtLine": 9,
"failureDetail": "expected 200, got 500",
"durationMs": 812,
"errorMessage": null,
"checkedAt": 1787000000000
},
{
"id": "flowres_xyz788",
"status": "up",
"testsTotal": 2,
"testsPassed": 2,
"testsFailed": 0,
"requestsMade": 3,
"failedTest": null,
"failedAtLine": null,
"failureDetail": null,
"durationMs": 640,
"errorMessage": null,
"checkedAt": 1786996400000
}
]
},
"meta": {
"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
"timestamp": "2026-08-31T10:30:00.000Z",
"pagination": {
"next": "eyJkIjoiYWZ0ZXIi",
"prev": null,
"perPage": 10
}
}
}