Quickstart
Monitor any scheduled job in under a minute. Create a check, add its one-line ping to your job, and LastPing emails you the moment the job misses its window — no alert routing to configure.
1 · Sign in
Sign in at app.lastping.dev with GitHub or with email. Everything below happens in the same console.
2 · Create a check
Click + New check, set how often the job runs and a grace period (how long to wait past the deadline before it counts as late). Your verified email is attached as an alert destination automatically — so a brand-new check can already reach you.
3 · Add the ping to your job
Append the check's ping URL to the end of your cron, backup, or CI job:
# at the very end of your job, once it has succeeded
curl -fsS https://ping.lastping.dev/<your-check-id>
Miss the window and you get an email; recover and you get the all-clear. Add Telegram, Slack, Discord, or webhook destinations any time.
curl quiet on success, fail on HTTP errors,
but still show real errors — the recommended flags for a ping in a job.Send your first ping
A ping is a plain HTTP request to your check's URL. The path decides what it means. Each check has its own unique ping URL — you'll find it on the check page.
Signal types
| Meaning | Request |
|---|---|
| Success (job finished OK) | curl -fsS https://ping.lastping.dev/<id> |
| Job started | curl -fsS https://ping.lastping.dev/<id>/start |
| Explicit failure | curl -fsS https://ping.lastping.dev/<id>/fail |
| Exit-code form | curl -fsS https://ping.lastping.dev/<id>/$? |
The exit-code form reads the number: 0 is treated as success, any non-zero
value as a failure. Put it right after the command you want to report on so $?
holds that command's exit status.
Shell example
# run the real work, then report whether it succeeded
/usr/local/bin/backup.sh
curl -fsS https://ping.lastping.dev/<id>/$?
Start & finish (with a run id)
Send a start ping before the work and a success/fail ping after. Add an
optional ?rid=<run-id> to correlate the two halves of one run:
RID=$(date +%s) curl -fsS "https://ping.lastping.dev/<id>/start?rid=$RID" /usr/local/bin/backup.sh curl -fsS "https://ping.lastping.dev/<id>/$??rid=$RID"
Cron example
# /etc/cron.d/backup — ping only if the job exits 0
0 3 * * * root /usr/local/bin/backup.sh && curl -fsS https://ping.lastping.dev/<id>
Signing in
LastPing supports two sign-in methods, both landing in the same console:
- GitHub OAuth — one click, no password to manage.
- Email + password — sign up, verify your email, then set a password.
One account, either way
You can add a password to a GitHub-created account, or link an email/password account to GitHub, as long as the email is verified and matches. Your verified email doubles as your first alert destination.
Checks & schedules
A check expects a ping on a schedule. If the ping doesn't arrive in time, the check goes late and then down, and LastPing alerts you.
Schedule kinds
- Simple interval — "expected every N minutes / hours / days".
- Cron — a cron expression, evaluated in a timezone you choose.
On top of the schedule, a grace period gives the job slack before it's considered late — set it to cover normal run-time jitter.
Statuses
Every check is in one of four states:
| Status | Meaning |
|---|---|
| new | Created, no ping received yet. |
| up | Pinged on time; healthy. |
| late | Past the deadline, still inside grace. |
| down | Grace exhausted; an incident is open and you're alerted. |
Pause, resume & the ping URL
Pause or resume a check from its page — a paused check never alerts, regardless of ping status. Each check has one unique ping URL that never changes; that's the URL you put in your job.
Alerts & destinations
When you create your first check, your verified email is auto-attached for down and recovery events — so the first check you ever make can already reach you without any setup.
Destinations
Add more under Destinations. LastPing supports:
- Telegram
- Slack
- Discord
- Webhook
Event types
| Event | When it fires |
|---|---|
| down | A check misses its window and an incident opens. |
| recovery | A down check pings again — the all-clear. |
| fail | A job reports an explicit failure (a /fail or non-zero exit-code ping). |
Send test & the alerts log
Send test fires a real alert through the pipeline so you can prove a destination actually works. A check with no destination shows a loud "won't alert anyone" warning. The Recent alerts log on the check shows what was sent, where, and whether it delivered.
Monitor a CI/CD pipeline
Instead of curling a ping URL at the end of a job script, you can point a provider webhook at LastPing. Every workflow run — pass or fail — is captured as a ping event; missed runs alert with a deep link to the failing run.
Setup overview
The webhook URL has the form:
https://ping.lastping.dev/ci/<provider>/<check-id>
where <provider> is github, gitlab, or
jenkins.
GitHub Actions
Go to your repository on GitHub, then Settings → Webhooks → Add webhook:
- Payload URL — paste the webhook URL.
- Content type — select
application/json. - Secret — paste the one-time secret.
- Under Which events would you like to trigger this webhook? choose Let me select individual events and tick Workflow runs and Workflow jobs (uncheck Pushes which is pre-ticked).
- Click Add webhook.
.github/workflows YAML are
needed. A missed or failed run alerts with a direct link to the run log.{failing_stage} stays
empty. workflow_run tells us a run failed; only workflow_job
carries the per-step conclusions, so it is the sole source of the step name in your alerts.
Without it everything still works — the alert just says “failed on main” instead of
“failed at Run make test on main”. Job events never count as pings: they are stored
against the run and read back when the run itself completes.GitLab CI
Go to your project on GitLab, then Settings → Webhooks:
- URL — paste the webhook URL.
- Secret token — paste the one-time secret.
- Under Trigger tick Pipeline events.
- Click Add webhook.
.gitlab-ci.yml changes are needed.Jenkins
Use the Notification Plugin (or any generic outbound webhook mechanism) to POST to the webhook URL. Include the secret in the request header:
X-LastPing-Token: <your-secret>
With the Notification Plugin, add a Job Notification endpoint in the job
configuration: set the URL to the webhook URL, Protocol HTTP, Format
JSON, and add the header above. No Jenkinsfile changes are
required.
{failing_stage} is always empty on Jenkins. The
Notification Plugin payload reports the build's overall result and carries no per-stage or
per-step detail, so there is nothing for LastPing to name. Every other CI token
({run_url}, {branch}, {commit},
{duration}) is populated as usual. If you need the failing stage in a Jenkins
alert, POST it yourself to the /ci/{id}/detail endpoint from a
post { failure { … } } block.Status pages
Publish the health of your checks on a shareable page. Go to Status pages → New and choose public or private.
Public pages
A public page lives at https://lastping.dev/status/<slug>
(also reachable at app.lastping.dev/status/<slug>). It shows per-check
status, 90-day uptime, and incident history.
Badges
Embed a status badge anywhere with:
https://lastping.dev/badge/<slug>/<check>
Troubleshooting
No alert arrived
Does the check have a verified destination? The check page warns loudly if not. Is the destination itself verified/connected? Use Send test to fire a real alert and confirm the whole path works.
Check stuck late or down
Confirm the job actually pings on success, and that the schedule and grace period match how the job really runs. A job that succeeds but never pings will always look down.
Ping isn't registering
Double-check the URL and check-id. A bare visit to ping.lastping.dev from a
browser redirects humans to the marketing site — that's expected; the ping still needs the
full /<id> path.
Setup guides
Step-by-step guides for wiring LastPing into whatever runs your job — usually one
curl ping at the end of the work, or one signed webhook for CI. Pick your
language, platform, scheduler, or CI system:
Cloud & serverless
Schedulers & frameworks
CI/CD pipelines
AI agents
MCP server (for AI agents)
LastPing ships a Model Context Protocol
server, so an AI agent (Claude, Cursor, Windsurf, …) can create and manage monitors, query
incidents, and set up its own dead-man's-switch in a single conversation — no dashboard
trip. It's hosted at https://mcp.lastping.dev; the agent connects with a URL and
your API key as a Bearer token.
Connect a client
Add this to your MCP client config (e.g. Claude Desktop's
claude_desktop_config.json). Get an API key in Settings → API keys.
{ "mcpServers": { "lastping": {
"url": "https://mcp.lastping.dev",
"headers": { "Authorization": "Bearer lp_your_key_here" } } } }
For Claude Code:
claude mcp add --transport http --scope user lastping https://mcp.lastping.dev --header "Authorization: Bearer lp_…"
Install the Terraform provider
LastPing has an official Terraform provider, published on the public Terraform Registry as
lastping-dev/lastping.
Declare your monitors, destinations, alert routing and status pages as code and keep them in
the same repository as the jobs they watch. Nothing to vendor, nothing to build.
1 · Require the provider
terraform {
required_providers {
lastping = {
source = "lastping-dev/lastping"
version = "~> 0.1"
}
}
}
provider "lastping" {
# Set LASTPING_API_KEY in your environment instead of hardcoding it.
}
2 · terraform init
Terraform downloads the provider from the public registry and verifies its signature — the same flow as any other registry provider.
$ terraform init
- Finding lastping-dev/lastping versions matching "~> 0.1"...
- Installing lastping-dev/lastping v0.1.0...
- Installed lastping-dev/lastping v0.1.0 (self-signed, key ID E863B1802B5C1E64)
Terraform has been successfully initialized!
lastping_api_key. The provider is
versioned 0.x, so pin with ~> 0.1 and re-check your plan when a
new minor ships.Authentication
Create an API key in Settings → API keys. It is shown once, so copy it then. The provider is scoped to the project that key belongs to — one key, one project, no ambiguity about what a plan is about to change.
Environment variables
Leave api_key out of the provider block on purpose: the provider reads
LASTPING_API_KEY from the environment, so the credential never sits in a file
you might commit.
export LASTPING_API_KEY="lp_…"
# Self-hosting? Point the provider somewhere else. Defaults to https://app.lastping.dev
export LASTPING_ENDPOINT="https://lastping.internal.example.com"
Secrets end up in Terraform state
Destination credentials and any key created by the managed
lastping_api_key resource are written to state in plaintext.
sensitive = true only obscures CLI output — it does not affect storage.
Use a remote backend with encryption at rest and restricted access, and treat the state file
as a credential store. For a credential Terraform only needs during its own run, prefer the
ephemeral lastping_api_key below.# Never hardcode a destination secret. Pass it in and mark it sensitive.
variable "slack_webhook_url" {
type = string
sensitive = true
}
Ephemeral keys — a credential that does not outlive the run
The ephemeral lastping_api_key mints a key for the duration of one run, hands
it to a provider configuration, and revokes it when the run ends. Nothing about it is written
to plan or state. Requires Terraform 1.10 or later.
# Mints a key that exists only for this run and is revoked when the run ends. # Nothing about it is written to plan or state. Requires Terraform 1.10+. ephemeral "lastping_api_key" "run" { name = "terraform-run" ttl = "30m" } provider "lastping" { alias = "run" api_key = ephemeral.lastping_api_key.run.key } resource "lastping_monitor" "nightly_backup" { provider = lastping.run name = "Nightly backup" slug = "nightly-backup" schedule_kind = "cron" cron_expr = "0 3 * * *" tz = "UTC" grace_s = 1800 }
The ttl is the safety net: if a run is killed before it can revoke its key,
the key still expires server-side on its own. There is no endpoint to extend a key, so set
ttl to comfortably exceed your longest apply. Minting the ephemeral key itself
needs a credential, so the default provider still reads LASTPING_API_KEY —
what changes is that every resource operation runs under a key that expires.
Resource reference
Six managed resources, one ephemeral resource, and six data sources. Every attribute is documented on the registry docs ↗; the working shapes are below.
lastping_monitor
A heartbeat, CI or HTTP probe monitor. name is the only required attribute,
but the API requires grace_s for heartbeat and ci
monitors and validates it to [60, 31536000] seconds. slug is the
stable, project-scoped identifier used for import.
resource "lastping_monitor" "nightly_backup" {
name = "Nightly backup"
slug = "nightly-backup"
schedule_kind = "cron" # or "simple" with period_s
cron_expr = "0 3 * * *"
tz = "UTC"
grace_s = 1800 # required; [60, 31536000]
tags = ["env:prod", "team:platform"]
runaway_ceiling = 5 # optional cap on pings per rolling hour
}
For an HTTP probe monitor, LastPing fetches the URL for you instead of waiting for an inbound ping:
resource "lastping_monitor" "public_api" {
name = "Public API health"
slug = "public-api-health"
monitor_type = "http"
probe_url = "https://api.example.com/healthz"
probe_method = "GET"
probe_interval_s = 60
probe_expected_status = 200
probe_expected_body = "ok"
probe_timeout_s = 5
}
2 × probe_interval_s so one slow probe cannot false-fire. Omit
grace_s to take the floor; set it only to ask for more than the floor, as
a smaller value is rejected at plan time rather than silently raised.Read-only attributes include id, ping_url,
status, due_at, alert_after and
last_ping_at. Feed ping_url to whatever runs the job and the loop
is closed.
lastping_destination
Where alerts are delivered. kind is one of webhook,
telegram, discord, slack, msteams,
googlechat, ntfy, pushover, email.
Credentials are write-only: stored, never returned.
# var.slack_webhook_url and var.webhook_signing_secret are sensitive variables, # declared as shown under Authentication above. resource "lastping_destination" "oncall_slack" { kind = "slack" # discord / msteams / googlechat take webhook_url too name = "#oncall" webhook_url = var.slack_webhook_url } resource "lastping_destination" "ops_email" { kind = "email" name = "Ops mailing list" address = "ops@example.com" # stays unverified until someone clicks the link } resource "lastping_destination" "internal_hook" { kind = "webhook" name = "Internal alert bus" url = "https://alerts.internal.example.com/lastping" secret = var.webhook_signing_secret }
Per-kind credential attributes: webhook_url (slack, discord, msteams,
googlechat), url + secret (webhook), bot_token +
chat_id (telegram), topic_url + optional token
(ntfy), token + user_key (pushover), address
(email).
lastping_route
One monitor's alerts for one event type, routed to a list of destinations. Event types
are down, recovery and fail. Use one resource per
event type — the API replaces the whole destination list on every write, so two
resources on the same pair would overwrite each other.
resource "lastping_route" "backup_down" {
monitor_id = lastping_monitor.nightly_backup.id
event_type = "down"
destination_ids = [lastping_destination.oncall_slack.id]
}
# An empty list is valid and means "deliver nowhere for this event" — a way to
# mute one event type without deleting the route.
resource "lastping_route" "backup_fail" {
monitor_id = lastping_monitor.nightly_backup.id
event_type = "fail"
destination_ids = []
}
lastping_alert_template
Custom alert message bodies for one monitor. One resource owns the monitor's whole template map, because the API replaces the set rather than merging into it.
resource "lastping_alert_template" "nightly_backup" {
monitor_id = lastping_monitor.nightly_backup.id
templates = {
"down" = "{check_name} has not checked in. Last ping {last_ping}, expected {schedule}. {incident_url}"
"recovery" = "{check_name} recovered."
# "event/cause" narrows it; the more specific key wins.
"down/silence" = "No ping from {check_name} since {last_ping}."
}
}
{token}, not Go templates. The tokens
are {check_name}, {status}, {event},
{cause}, {last_ping}, {schedule},
{incident_url}, {run_url}, {branch},
{commit}, {actor}, {failing_stage},
{duration}, {latency}, {status_code} and
{url}. {failing_stage} is populated from GitLab pipeline hooks and
from GitHub workflow_job events (subscribe to Workflow jobs as well as
Workflow runs); it is always empty on Jenkins, whose Notification Plugin payload has
no step detail. A body containing {{.CheckName}} is accepted — it
does not match the token pattern, so validation has nothing to object to — and then renders
literally into a real alert. An unrecognised token in {token} shape, such as
{check_nam}, fails the apply and names the offending key.lastping_status_page
A public or private status page over a chosen set of monitors. title is
required; omit slug on a private page and the server generates one.
resource "lastping_status_page" "public" {
slug = "acme-platform"
title = "Acme Platform Status"
visibility = "public" # "private" is the default
check_ids = [lastping_monitor.nightly_backup.id]
lifecycle {
create_before_destroy = true
}
}
slug forces replacement and Terraform destroys before creating by
default, which would release your slug into that global namespace before claiming the new
one. Always set create_before_destroy on a public page. Free for individuals
allows one public status page per project; private pages are unlimited.lastping_api_key (and the ephemeral one)
A long-lived key for automation that outlives the run. The plaintext lands in state — see Authentication — so prefer the ephemeral resource when Terraform only needs the credential for itself. The API mints a key once and never lets it be read again, so every attribute forces replacement and there is no import.
resource "lastping_api_key" "ci" {
name = "github-actions"
expires_at = "2027-01-01T00:00:00Z" # optional, RFC 3339, must be in the future
lifecycle {
create_before_destroy = true # rotation: new key before the old one is revoked
}
}
Data sources
| Data source | What it reads |
|---|---|
lastping_project | The project the configured key belongs to. Doubles as a credential smoke-test at plan time. |
lastping_monitors | Every monitor in the project, optionally filtered by tag. |
lastping_monitor | One monitor by id or slug — how you route to a monitor Terraform does not manage. |
lastping_destination | One destination by id or name. An ambiguous name is an error, not an arbitrary pick. |
lastping_incidents | One monitor's incident history, newest first. For outputs and reports only. |
lastping_metrics | The project's monitors as Prometheus gauges, exactly as /metrics returns them. |
data "lastping_monitors" "agents" {
tag = "agent:claude"
}
output "agent_monitor_count" {
value = length(data.lastping_monitors.agents.monitors)
}
Import and export
You almost certainly have monitors already. You do not have to rewrite them by hand, and you should not let Terraform recreate them.
terraform import
Creating a monitor whose slug already exists fails the apply rather than silently taking it over. Import it instead. Routes and alert templates behave the same way: Terraform refuses to adopt what it did not create, because one write replaces the whole set and would overwrite someone else's alerting.
| Resource | Import ID |
|---|---|
lastping_monitor | Slug (preferred) or UUID — nightly-backup |
lastping_destination | UUID, or the name when exactly one destination has it |
lastping_route | <monitor-uuid>:<event-type> |
lastping_alert_template | The monitor's UUID |
lastping_status_page | UUID, or the slug within your own project |
lastping_api_key | Not importable — an imported key could never populate key |
terraform import lastping_monitor.nightly_backup nightly-backup terraform import lastping_route.backup_down 550e8400-e29b-41d4-a716-446655440000:down terraform import lastping_alert_template.nightly_backup 550e8400-e29b-41d4-a716-446655440000
Importing cannot recover credentials — the API never returns them — so a destination comes back with its secret attribute empty, and the first apply after the import writes the configured value back as an in-place update.
Export as Terraform
Rather than writing the HCL yourself, have LastPing generate it. The export renders your
existing monitors, destinations, routes, alert templates and status pages as HCL and pairs
every resource with a Terraform import block — so adopting Terraform
adopts what you already have instead of proposing to recreate it.
GET /api/v1/export/terraform — optional tag,
monitor_slug and include query parameters.export_terraform tool — how an agent hands work it created over
MCP back to a Terraform-managed repo.$ curl -fsS -H "Authorization: Bearer $LASTPING_API_KEY" \
https://app.lastping.dev/api/v1/export/terraform > lastping.tf
What comes out:
# Generated by LastPing. # Secrets are not exported — the API never returns them. Every `var.` # reference below has a matching variable block you must fill in. # Import blocks adopt your existing resources instead of recreating them. import { to = lastping_monitor.nightly_backup id = "nightly-backup" } resource "lastping_monitor" "nightly_backup" { name = "Nightly backup" slug = "nightly-backup" schedule_kind = "cron" cron_expr = "0 3 * * *" tz = "UTC" grace_s = 1800 tags = ["env:prod", "team:platform"] } import { to = lastping_destination.oncall id = "29145b3c-64c7-4b7f-91b7-a09c0eecaee1" } resource "lastping_destination" "oncall" { kind = "slack" name = "#oncall" webhook_url = var.slack_webhook_url_oncall } variable "slack_webhook_url_oncall" { # TODO: set this — not exported, the API never returns secrets. type = string sensitive = true description = "webhook_url for destination \"#oncall\"" }
Fill in the variables, then terraform plan. A clean
Plan: 0 to add, 0 to change, 0 to destroy. is the whole point: your monitoring
is now in version control and nothing about it changed.
Full example
A complete monitoring stack in one file: provider, monitor, destination, routing and alert wording. This is not a fragment — it applies as-is against the real API.
terraform {
required_providers {
lastping = {
source = "lastping-dev/lastping"
version = "~> 0.1"
}
}
}
provider "lastping" {
# Reads LASTPING_API_KEY from the environment.
}
# The Slack incoming-webhook URL is itself the credential. Keep it out of the
# repo: pass it with TF_VAR_slack_webhook_url or a secrets manager.
variable "slack_webhook_url" {
type = string
sensitive = true
}
# 1 — the monitor. It expects a ping every night at 03:00 UTC and waits
# grace_s seconds past that before opening an incident.
resource "lastping_monitor" "nightly_backup" {
name = "Nightly backup"
slug = "nightly-backup"
schedule_kind = "cron"
cron_expr = "0 3 * * *"
tz = "UTC"
grace_s = 1800
tags = ["env:prod", "team:platform"]
}
# 2 — where alerts go.
resource "lastping_destination" "oncall_slack" {
kind = "slack"
name = "#oncall"
webhook_url = var.slack_webhook_url
}
# 3 — which events go there. One resource per (monitor, event type).
resource "lastping_route" "backup_down" {
monitor_id = lastping_monitor.nightly_backup.id
event_type = "down"
destination_ids = [lastping_destination.oncall_slack.id]
}
resource "lastping_route" "backup_recovery" {
monitor_id = lastping_monitor.nightly_backup.id
event_type = "recovery"
destination_ids = [lastping_destination.oncall_slack.id]
}
# 4 — what the alert says. Placeholders are {token}, not Go templates.
resource "lastping_alert_template" "nightly_backup" {
monitor_id = lastping_monitor.nightly_backup.id
templates = {
"down" = "{check_name} has not checked in. Last ping {last_ping}, expected {schedule}. {incident_url}"
"recovery" = "{check_name} recovered."
}
}
# 5 — the ping URL for the job itself.
output "backup_ping_url" {
value = lastping_monitor.nightly_backup.ping_url
}
terraform apply creates five resources. Feed
backup_ping_url to whatever runs the backup — see
Send your first ping — and the loop is closed.
Terraform in CI/CD
Once monitoring is code, the pipeline that deploys the job can apply the monitor that watches it. Rename the job and its monitor is renamed with it; delete the job and the monitor goes too. There is no state in which a monitor sits green because the thing it watched stopped existing.
GitHub Actions — plan on PR, apply on main
Store your key as the repository secret LASTPING_API_KEY
(Settings → Secrets and variables → Actions). Configure a remote state backend with
encryption at rest before running this against anything real — see
Authentication.
name: Monitoring
on:
pull_request:
paths: [ "monitoring/**" ]
push:
branches: [ main ]
paths: [ "monitoring/**" ]
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: monitoring
env:
LASTPING_API_KEY: ${{ secrets.LASTPING_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.10.5 # 1.10+ for the ephemeral key
- run: terraform init
- run: terraform plan -input=false
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
Running under a run-scoped key
To keep the long-lived key out of every resource operation, mint an ephemeral key at the
start of the run and point an aliased provider at it. The stored
LASTPING_API_KEY is then used only to mint that key, and the credential the
apply actually runs under is revoked when the job finishes.
# Requires Terraform 1.10 or later.
ephemeral "lastping_api_key" "ci" {
name = "github-actions-run"
ttl = "30m"
}
provider "lastping" {
alias = "ci"
api_key = ephemeral.lastping_api_key.ci.key
}
resource "lastping_monitor" "nightly_backup" {
provider = lastping.ci
name = "Nightly backup"
slug = "nightly-backup"
schedule_kind = "cron"
cron_expr = "0 3 * * *"
tz = "UTC"
grace_s = 1800
}
API reference
LastPing has a REST API under /api/v1 (base
https://app.lastping.dev). Authenticate every request with an API key in the
X-Api-Key header. Create a key in Settings → API keys; it's shown once,
so copy it then.
Main resources
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/whoami | Verify the key; returns your project id. |
| POST | /api/v1/checks | Create a check (upsert by slug). |
| GET | /api/v1/checks | List all checks. |
| GET | /api/v1/checks/{id} | Get one check. |
| PATCH | /api/v1/checks/{id} | Update a check's schedule. |
| DELETE | /api/v1/checks/{id} | Delete a check. |
| POST | /api/v1/checks/{id}/pause | Pause a check (stop alerting). |
| POST | /api/v1/checks/{id}/resume | Resume a paused check. |
| POST | /api/v1/channels | Create a notification channel (webhook or telegram kind). |
| GET | /api/v1/channels | List channels. |
| PUT | /api/v1/checks/{id}/routes/{event_type} | Route an event type to channels. |
| GET | /api/v1/checks/{id}/pings | List recent pings. |
| GET | /api/v1/checks/{id}/incidents | List incidents. |
Event types for routes are down, recovery, and fail.
Channel secrets (webhook secret, Telegram bot_token) are
write-only and never returned.
CLI & curl recipes
Practical snippets using curl and your X-Api-Key. Set your key
once and reuse it:
export LP_KEY="lp_your_api_key_here" export LP_BASE="https://app.lastping.dev"
Verify your key
curl -fsS "$LP_BASE/api/v1/whoami" \ -H "X-Api-Key: $LP_KEY"
Create (or upsert) a check
Passing a slug makes this idempotent: a matching slug updates the existing
check and returns 200 instead of 201.
curl -fsS "$LP_BASE/api/v1/checks" \
-H "X-Api-Key: $LP_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily backup job",
"slug": "daily-backup",
"schedule_kind": "simple",
"period_s": 86400,
"grace_s": 600
}'
For a cron schedule, use "schedule_kind":"cron" with
"cron_expr":"0 3 * * *" and an optional "tz":"Europe/London".
List checks
curl -fsS "$LP_BASE/api/v1/checks" \ -H "X-Api-Key: $LP_KEY"
Ping a check
Pings don't use the API key — they're a plain GET to the check's ping URL:
curl -fsS https://ping.lastping.dev/<check-id>
Create a channel
The API creates webhook and telegram channels. Email, Slack, and Discord destinations are configured in the app under Destinations.
curl -fsS "$LP_BASE/api/v1/channels" \
-H "X-Api-Key: $LP_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "webhook",
"name": "Ops webhook",
"config": { "url": "https://example.com/hook", "secret": "s3cr3t" }
}'
Attach a route
Route the down event on a check to one or more channels
(channel_ids is the complete list; an empty list clears it):
curl -fsS -X PUT \
"$LP_BASE/api/v1/checks/<check-id>/routes/down" \
-H "X-Api-Key: $LP_KEY" \
-H "Content-Type: application/json" \
-d '{ "channel_ids": ["c1d2e3f4-0000-0000-0000-000000000001"] }'