Reconcile a discovery scan into monitors
Takes everything one scan of a repo or a host found and reconciles it against what this project already monitors: new sources become monitors, already-monitored sources are reported untouched, and monitors whose source has vanished are reported and never deleted.
This is what makes discovery re-runnable. A scan you can only run once is a setup wizard. A scan that is safe to re-run — nightly, on every CI build, on every deploy — is drift detection: "you added a scheduled workflow last week and nothing is watching it." Point it at the same repo every night and the answer converges.
The project comes from the API key, like every other endpoint here. There is no project id in the path.
The three rules
- A source that is new becomes a monitor and is returned under created.
- A source that is already monitored is returned under existing and is not modified — not its name, not its schedule, not its thresholds. Whatever a user tuned by hand survives every future scan.
- A monitor whose source is absent from the payload is returned under orphaned and is never deleted. Reporting it is the whole point; deleting it would mean a scanner bug or a wrong branch could tear down production monitoring.
Monitors created by hand carry no source identity. They are invisible to reconcile in both directions: a scan can neither adopt one nor report one as orphaned.
What a created monitor looks like
- schedule_cron present → a cron monitor on that expression, in the source's tz (default UTC), so LastPing arms a deadline from the same expression the job runs on. tz is required for crontab and systemd-timer sources that carry a schedule_cron: they fire in host-local time, and a UTC-assumed deadline on a host that is not on UTC is a false incident every day. Sending "UTC" explicitly satisfies it; omitting it is a 400.
- schedule_cron absent → an on_demand monitor. A push-triggered workflow has no cadence to be late against.
- expect_every_s (the silence floor) is seeded from the longest gap between consecutive fire times of schedule_cron, times two. The longest gap, because cron intervals are routinely irregular — 0 9 * * 1-5 alternates 24-hour gaps with a 72-hour one, and a floor set from the short gap would page every weekend. An explicit suggested_expect_every_s overrides it.
- grace_s is 300. A scanner has no way to know how late a job is allowed to be; that is a judgement about the business, not about the repository. PATCH it once and reconcile will never touch it again.
- No slug, and monitor_from is unset — so a freshly created monitor is not armed until its first ping, rather than paging minutes after every scan for jobs nobody has wired a ping into yet.
All or nothing
The write half runs in one transaction. Either every new monitor exists or none does — a half-created fleet would leave the caller unable to tell which half landed.
Request
- Base URL: https://app.lastping.dev
- URL: https://app.lastping.dev/api/v1/discovery/reconcile
- Auth: API key in header X-Api-Key
Request body
Example request
{
"sources": [
{
"source_kind": "github-actions",
"source_ref": ".github/workflows/nightly.yml#build",
"name": "nightly build",
"schedule_cron": "0 3 * * *",
"tz": "Europe/Berlin",
"suggested_expect_every_s": 90000
}
]
}Response
The reconcile diff. 200 rather than 201 even when monitors were created: the response is a report, and a run that creates nothing is the success case, not an empty result.
Example response
{
"created": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Daily backup job",
"slug": "daily-backup",
"schedule_kind": "simple",
"period_s": 86400,
"cron_expr": "0 3 * * *",
"tz": "UTC",
"grace_s": 600,
"max_runtime_s": 14400,
"step_timeout_s": 300,
"expect_every_s": 900,
"notify_min_run_s": 300,
"blocked_timeout_s": 86400,
"failure_threshold": 3,
"monitor_from": "2026-01-01T00:00:00Z",
"status": "up",
"last_ping_at": "2026-07-10T03:00:05Z",
"due_at": "2026-07-11T03:00:00Z",
"alert_after": "2026-07-11T03:10:00Z",
"maintenance_until": "2026-07-11T09:00:00Z",
"created_at": "2026-07-01T12:00:00Z",
"ping_url": "https://ingest.lastping.dev/550e8400-e29b-41d4-a716-446655440000",
"monitor_type": "http",
"probe_url": "https://example.com/healthz",
"probe_method": "GET",
"probe_expected_body": "\"status\":\"ok\"",
"probe_interval_s": 60,
"probe_expected_status": 200,
"probe_timeout_s": 10,
"next_probe_at": "2026-07-13T03:01:00Z",
"ci_provider": "github",
"ci_configured": true,
"ci_webhook_url": "https://ingest.lastping.dev/ci/github/550e8400-e29b-41d4-a716-446655440000",
"ci_secret": "a3f8c2d1e4b7a9f0c3d2e1b4a7f8c0d3e2b1a4f7",
"ci_workflow": "ci.yml",
"ci_branch": "main",
"ci_ignored": {
"count": 12,
"last_at": "2026-10-08T14:02:11Z",
"last_workflow": "CI",
"last_branch": "feature/login",
"last_matched_at": "2026-10-01T09:00:00Z",
"filters_changed_at": "2026-09-20T12:00:00Z",
"same_workflow_count": 3,
"same_workflow_last_at": "2026-10-08T11:40:00Z",
"same_workflow_last_branch": "release/2.4",
"new_workflow": "Deploy prod",
"new_workflow_first_at": "2026-10-06T09:12:00Z",
"new_workflow_count": 2
},
"runaway_ceiling": 20,
"tags": [
"agent:claude",
"env:prod"
],
"agent_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"source_kind": "github-actions",
"source_ref": ".github/workflows/nightly.yml#build",
"trace_content": "dropped",
"kind": "cron",
"uptime_90d": 99.67,
"latency_ms": 142,
"routes_count": 2,
"recent_outcomes": [
"succeeded",
"succeeded",
"failed"
],
"hook_version": 2,
"hook_version_current": 2
}
],
"existing": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Daily backup job",
"slug": "daily-backup",
"schedule_kind": "simple",
"period_s": 86400,
"cron_expr": "0 3 * * *",
"tz": "UTC",
"grace_s": 600,
"max_runtime_s": 14400,
"step_timeout_s": 300,
"expect_every_s": 900,
"notify_min_run_s": 300,
"blocked_timeout_s": 86400,
"failure_threshold": 3,
"monitor_from": "2026-01-01T00:00:00Z",
"status": "up",
"last_ping_at": "2026-07-10T03:00:05Z",
"due_at": "2026-07-11T03:00:00Z",
"alert_after": "2026-07-11T03:10:00Z",
"maintenance_until": "2026-07-11T09:00:00Z",
"created_at": "2026-07-01T12:00:00Z",
"ping_url": "https://ingest.lastping.dev/550e8400-e29b-41d4-a716-446655440000",
"monitor_type": "http",
"probe_url": "https://example.com/healthz",
"probe_method": "GET",
"probe_expected_body": "\"status\":\"ok\"",
"probe_interval_s": 60,
"probe_expected_status": 200,
"probe_timeout_s": 10,
"next_probe_at": "2026-07-13T03:01:00Z",
"ci_provider": "github",
"ci_configured": true,
"ci_webhook_url": "https://ingest.lastping.dev/ci/github/550e8400-e29b-41d4-a716-446655440000",
"ci_secret": "a3f8c2d1e4b7a9f0c3d2e1b4a7f8c0d3e2b1a4f7",
"ci_workflow": "ci.yml",
"ci_branch": "main",
"ci_ignored": {
"count": 12,
"last_at": "2026-10-08T14:02:11Z",
"last_workflow": "CI",
"last_branch": "feature/login",
"last_matched_at": "2026-10-01T09:00:00Z",
"filters_changed_at": "2026-09-20T12:00:00Z",
"same_workflow_count": 3,
"same_workflow_last_at": "2026-10-08T11:40:00Z",
"same_workflow_last_branch": "release/2.4",
"new_workflow": "Deploy prod",
"new_workflow_first_at": "2026-10-06T09:12:00Z",
"new_workflow_count": 2
},
"runaway_ceiling": 20,
"tags": [
"agent:claude",
"env:prod"
],
"agent_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"source_kind": "github-actions",
"source_ref": ".github/workflows/nightly.yml#build",
"trace_content": "dropped",
"kind": "cron",
"uptime_90d": 99.67,
"latency_ms": 142,
"routes_count": 2,
"recent_outcomes": [
"succeeded",
"succeeded",
"failed"
],
"hook_version": 2,
"hook_version_current": 2
}
],
"orphaned": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Daily backup job",
"slug": "daily-backup",
"schedule_kind": "simple",
"period_s": 86400,
"cron_expr": "0 3 * * *",
"tz": "UTC",
"grace_s": 600,
"max_runtime_s": 14400,
"step_timeout_s": 300,
"expect_every_s": 900,
"notify_min_run_s": 300,
"blocked_timeout_s": 86400,
"failure_threshold": 3,
"monitor_from": "2026-01-01T00:00:00Z",
"status": "up",
"last_ping_at": "2026-07-10T03:00:05Z",
"due_at": "2026-07-11T03:00:00Z",
"alert_after": "2026-07-11T03:10:00Z",
"maintenance_until": "2026-07-11T09:00:00Z",
"created_at": "2026-07-01T12:00:00Z",
"ping_url": "https://ingest.lastping.dev/550e8400-e29b-41d4-a716-446655440000",
"monitor_type": "http",
"probe_url": "https://example.com/healthz",
"probe_method": "GET",
"probe_expected_body": "\"status\":\"ok\"",
"probe_interval_s": 60,
"probe_expected_status": 200,
"probe_timeout_s": 10,
"next_probe_at": "2026-07-13T03:01:00Z",
"ci_provider": "github",
"ci_configured": true,
"ci_webhook_url": "https://ingest.lastping.dev/ci/github/550e8400-e29b-41d4-a716-446655440000",
"ci_secret": "a3f8c2d1e4b7a9f0c3d2e1b4a7f8c0d3e2b1a4f7",
"ci_workflow": "ci.yml",
"ci_branch": "main",
"ci_ignored": {
"count": 12,
"last_at": "2026-10-08T14:02:11Z",
"last_workflow": "CI",
"last_branch": "feature/login",
"last_matched_at": "2026-10-01T09:00:00Z",
"filters_changed_at": "2026-09-20T12:00:00Z",
"same_workflow_count": 3,
"same_workflow_last_at": "2026-10-08T11:40:00Z",
"same_workflow_last_branch": "release/2.4",
"new_workflow": "Deploy prod",
"new_workflow_first_at": "2026-10-06T09:12:00Z",
"new_workflow_count": 2
},
"runaway_ceiling": 20,
"tags": [
"agent:claude",
"env:prod"
],
"agent_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"source_kind": "github-actions",
"source_ref": ".github/workflows/nightly.yml#build",
"trace_content": "dropped",
"kind": "cron",
"uptime_90d": 99.67,
"latency_ms": 142,
"routes_count": 2,
"recent_outcomes": [
"succeeded",
"succeeded",
"failed"
],
"hook_version": 2,
"hook_version_current": 2
}
]
}Changes
No recorded changes to this operation across all 2 revisions of this API.