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

json
{
	"id": "flow_abc123",
	"name": "Sign in and read profile",
	"intervalSeconds": 3600,
	"isEnabled": true,
	"lastCheckedAt": 1787000000000,
	"lastStatus": "up",
	"createdAt": 1786300000000,
	"updatedAt": 1786400000000
}
json
{
	"$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

GET/api/v1/flow-monitors

Retrieves 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

  • perPageinteger

    Results per page, 1-200 (default: 50)

  • cursorstring

    Page to fetch, taken from a Link header

Errors (3)
StatusTypeMeaning
400bad-requestThe request is malformed
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
Response fields (200)
  • dataobjectRequired
    Show nested fields
    • flowMonitorsFlowMonitor[]Required
      Show nested fields
      • idstringRequired
      • namestringRequired
      • intervalSecondsintegerRequired

        One of 900, 1800, 3600, 10800, 21600, 43200, 86400.

      • isEnabledbooleanRequired
      • lastCheckedAtinteger | nullRequired

        Epoch milliseconds

      • lastStatusstring | nullRequired

        One of up, down, error.

      • createdAtintegerRequired

        Epoch milliseconds

      • updatedAtintegerRequired

        Epoch milliseconds

  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

    • paginationobjectRequired
      Show nested fields
      • nextstring | nullRequired

        Cursor of the following page

      • prevstring | nullRequired

        Cursor of the preceding page

      • perPageintegerRequired
      • totalinteger
Terminal
curl -i "https://uptime.sergiodxa.com/api/v1/flow-monitors?perPage=25" \
  -H "Authorization: Bearer uptime_your_api_key"

Response

json
{
	"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

POST/api/v1/flow-monitors

Creates 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

  • namestringRequired

    Monitor name (1-255 characters)

  • sourcestringRequired

    The spec text, up to 20000 characters. Stored, never returned.

  • intervalSecondsinteger

    One of 900, 1800, 3600, 10800, 21600, 43200, 86400 (default: 3600)

  • isEnabledboolean

    Whether 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

Terminal
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 }'
json
{
	"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

Terminal
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}"
  }'
json
{
	"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:

json
{
	"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)
StatusTypeMeaning
400validation-errorThe request failed validation
400idempotency-key-invalidThe Idempotency-Key header is not valid
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
409idempotency-key-in-useA request with this idempotency key is still being processed
422idempotency-key-reusedThis idempotency key was already used for a different request
Response fields (201)
  • dataobjectRequired
    Show nested fields
    • flowMonitorobjectRequired
      Show nested fields
      • idstringRequired
      • namestringRequired
      • intervalSecondsintegerRequired

        One of 900, 1800, 3600, 10800, 21600, 43200, 86400.

      • isEnabledbooleanRequired
      • lastCheckedAtinteger | nullRequired

        Epoch milliseconds

      • lastStatusstring | nullRequired

        One of up, down, error.

      • createdAtintegerRequired

        Epoch milliseconds

      • updatedAtintegerRequired

        Epoch milliseconds

  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

Terminal
curl https://uptime.sergiodxa.com/api/v1/flow-monitors \
  -H "Authorization: Bearer uptime_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sign in and read profile",
    "source": "use http\ntest \"a member can sign in and read their profile\" {\n\twhen {\n\t\tlet session = http.post \"https://app.example.com/login\" { email: \"probe@example.com\", password: \"redacted\" }\n\t\tlet profile = http.get \"https://app.example.com/me\"\n\t}\n\tthen {\n\t\texpect session.status == 200\n\t\texpect profile.body.email == \"probe@example.com\"\n\t}\n}",
    "intervalSeconds": 3600
  }'
request.js
let response = await fetch("https://uptime.sergiodxa.com/api/v1/flow-monitors", {
	method: "POST",
	headers: {
		"Authorization": "Bearer uptime_your_api_key",
		"Content-Type": "application/json",
	},
	body: JSON.stringify({
		"name": "Sign in and read profile",
		"source": "use http\ntest \"a member can sign in and read their profile\" {\n\twhen {\n\t\tlet session = http.post \"https://app.example.com/login\" { email: \"probe@example.com\", password: \"redacted\" }\n\t\tlet profile = http.get \"https://app.example.com/me\"\n\t}\n\tthen {\n\t\texpect session.status == 200\n\t\texpect profile.body.email == \"probe@example.com\"\n\t}\n}",
		"intervalSeconds": 3600
	}),
});
let result = await response.json();

Response

json
{
	"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

GET/api/v1/flow-monitors/:flowMonitorId

Retrieves 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)
StatusTypeMeaning
400validation-errorThe request failed validation
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
404not-foundThe resource does not exist
Response fields (200)
  • dataobjectRequired
    Show nested fields
    • flowMonitorobjectRequired
      Show nested fields
      • idstringRequired
      • namestringRequired
      • intervalSecondsintegerRequired

        One of 900, 1800, 3600, 10800, 21600, 43200, 86400.

      • isEnabledbooleanRequired
      • lastCheckedAtinteger | nullRequired

        Epoch milliseconds

      • lastStatusstring | nullRequired

        One of up, down, error.

      • createdAtintegerRequired

        Epoch milliseconds

      • updatedAtintegerRequired

        Epoch milliseconds

  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

Terminal
curl https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123 \
  -H "Authorization: Bearer uptime_your_api_key"
request.js
let response = await fetch("https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123", {
	headers: {
		"Authorization": "Bearer uptime_your_api_key",
	},
});
let result = await response.json();

Response

json
{
	"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

PATCH/api/v1/flow-monitors/:flowMonitorId

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

  • namestring

    Monitor name (1-255 characters)

  • sourcestring

    Replacement spec text, up to 20000 characters

  • intervalSecondsinteger

    One of 900, 1800, 3600, 10800, 21600, 43200, 86400

  • isEnabledboolean

    Whether 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)
StatusTypeMeaning
400validation-errorThe request failed validation
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
404not-foundThe resource does not exist
Response fields (200)
  • dataobjectRequired
    Show nested fields
    • flowMonitorobjectRequired
      Show nested fields
      • idstringRequired
      • namestringRequired
      • intervalSecondsintegerRequired

        One of 900, 1800, 3600, 10800, 21600, 43200, 86400.

      • isEnabledbooleanRequired
      • lastCheckedAtinteger | nullRequired

        Epoch milliseconds

      • lastStatusstring | nullRequired

        One of up, down, error.

      • createdAtintegerRequired

        Epoch milliseconds

      • updatedAtintegerRequired

        Epoch milliseconds

  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

Terminal
curl https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123 \
  -X PATCH \
  -H "Authorization: Bearer uptime_your_api_key" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{ "intervalSeconds": 21600, "isEnabled": false }'
request.js
let response = await fetch("https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123", {
	method: "PATCH",
	headers: {
		"Authorization": "Bearer uptime_your_api_key",
		"Content-Type": "application/merge-patch+json",
	},
	body: JSON.stringify({
		"intervalSeconds": 21600,
		"isEnabled": false
	}),
});
let result = await response.json();

Response

json
{
	"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

DELETE/api/v1/flow-monitors/:flowMonitorId

Permanently deletes a flow monitor, its spec, and its run history.

Required scope: flow-monitors:delete

Errors (4)
StatusTypeMeaning
400validation-errorThe request failed validation
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
404not-foundThe resource does not exist
Response fields (200)
  • dataobjectRequired
    Show nested fields
    • deletedtrueRequired
  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

Terminal
curl https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123 \
  -X DELETE \
  -H "Authorization: Bearer uptime_your_api_key"
request.js
let response = await fetch("https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123", {
	method: "DELETE",
	headers: {
		"Authorization": "Bearer uptime_your_api_key",
	},
});
let result = await response.json();

Response

json
{
	"data": { "deleted": true },
	"meta": {
		"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
		"timestamp": "2026-08-31T10:30:00.000Z"
	}
}

Get Flow Monitor Results

GET/api/v1/flow-monitors/:flowMonitorId/results

Retrieves 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

  • perPageinteger

    Results per page, 1-200 (default: 50)

  • cursorstring

    Page to fetch, taken from a Link header

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)
StatusTypeMeaning
400bad-requestThe request is malformed
400validation-errorThe request failed validation
401unauthorizedThe API key is missing or invalid
403forbiddenThe API key lacks the required scope
404not-foundThe resource does not exist
Response fields (200)
  • dataobjectRequired
    Show nested fields
    • resultsFlowMonitorResult[]Required
      Show nested fields
      • idstringRequired
      • statusstringRequired

        One of up, down, error.

      • testsTotalintegerRequired
      • testsPassedintegerRequired
      • testsFailedintegerRequired
      • requestsMadeintegerRequired

        HTTP requests the run made; each bills

      • failedTeststring | nullRequired
      • failedAtLineinteger | nullRequired
      • failureDetailstring | nullRequired
      • durationMsinteger | nullRequired
      • errorMessagestring | nullRequired

        Why the run could not be performed; set with error only

      • checkedAtintegerRequired

        Epoch milliseconds

  • metaobjectRequired
    Show nested fields
    • requestIdstringRequired

      Format: uuid.

    • timestampstringRequired

      Format: date-time.

    • paginationobjectRequired
      Show nested fields
      • nextstring | nullRequired

        Cursor of the following page

      • prevstring | nullRequired

        Cursor of the preceding page

      • perPageintegerRequired
      • totalinteger
Terminal
curl -i "https://uptime.sergiodxa.com/api/v1/flow-monitors/flow_abc123/results?perPage=10" \
  -H "Authorization: Bearer uptime_your_api_key"

Response

json
{
	"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
		}
	}
}
Documentation