LastPing docs
Open app
Getting started

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.

-fsS makes curl quiet on success, fail on HTTP errors, but still show real errors — the recommended flags for a ping in a job.
Getting started

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

MeaningRequest
Success (job finished OK)curl -fsS https://ping.lastping.dev/<id>
Job startedcurl -fsS https://ping.lastping.dev/<id>/start
Explicit failurecurl -fsS https://ping.lastping.dev/<id>/fail
Exit-code formcurl -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>
Getting started

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.

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.

Guides

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:

StatusMeaning
newCreated, no ping received yet.
upPinged on time; healthy.
latePast the deadline, still inside grace.
downGrace 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.

Guides

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:

  • Email
  • Telegram
  • Slack
  • Discord
  • Webhook

Event types

EventWhen it fires
downA check misses its window and an incident opens.
recoveryA down check pings again — the all-clear.
failA 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.

Guides

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

1.Create a check in the app.
2.On the check page choose Monitor a CI/CD pipeline and select your provider.
3.Copy the webhook URL and the one-time secret shown on screen.
4.Paste them into your provider's webhook settings (details below).

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.
Note: no changes to your .github/workflows YAML are needed. A missed or failed run alerts with a direct link to the run log.
Tick Workflow jobs too, or {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.
Note: no .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.

Note: Jenkins has no native delivery-id header. LastPing deduplicates by a SHA-256 hash of the request body, so retried deliveries of the exact same payload are idempotent.
{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.
Guides

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>
Guides

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.

Integrations

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:

Languages & runtimes

Cloud & serverless

Schedulers & frameworks

CI/CD pipelines

AI agents

Don't see yours? Any language that can make an HTTP request works — see Send your first ping for the bare pattern, or browse all guides ↗.
Integrations

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_…"

Full MCP reference: lastping.dev/mcp ↗ — per-client configs (Claude Desktop/Code, Cursor, Windsurf), the complete tool list, and a prompt cookbook.
Monitoring as Code

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!
Version requirements: Terraform 1.5 or later for everything here, and 1.10 or later if you use the ephemeral lastping_api_key. The provider is versioned 0.x, so pin with ~> 0.1 and re-check your plan when a new minor ships.
Monitoring as Code

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 is not encryption. Marking a variable or an output 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.

Monitoring as Code

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
}
grace_s on HTTP monitors: the server floors the effective grace at 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}."
  }
}
Placeholders are {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
  }
}
Status page slugs are globally unique across every LastPing account, not scoped to your project — so a slug you have never seen may already be taken by a stranger. 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 sourceWhat it reads
lastping_projectThe project the configured key belongs to. Doubles as a credential smoke-test at plan time.
lastping_monitorsEvery monitor in the project, optionally filtered by tag.
lastping_monitorOne monitor by id or slug — how you route to a monitor Terraform does not manage.
lastping_destinationOne destination by id or name. An ambiguous name is an error, not an arbitrary pick.
lastping_incidentsOne monitor's incident history, newest first. For outputs and reports only.
lastping_metricsThe 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)
}
One deliberate gap: binding a monitor to a CI provider is not manageable through Terraform. Doing so mints a write-once secret the API returns only at creation and can never read back, so the provider does not model it. Configure CI binding in the dashboard — see Monitor a CI/CD pipeline.
Monitoring as Code

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.

ResourceImport ID
lastping_monitorSlug (preferred) or UUID — nightly-backup
lastping_destinationUUID, or the name when exactly one destination has it
lastping_route<monitor-uuid>:<event-type>
lastping_alert_templateThe monitor's UUID
lastping_status_pageUUID, or the slug within your own project
lastping_api_keyNot 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.

API GET /api/v1/export/terraform — optional tag, monitor_slug and include query parameters.
MCP the export_terraform tool — how an agent hands work it created over MCP back to a Terraform-managed repo.
UI Export as Terraform in the app, which downloads the same HCL as a file.
$ 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.

Monitoring as Code

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.

Monitoring as Code

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
}
Monitoring the pipeline itself is a separate feature and does not need Terraform: connect the repository once under CI/CD in the app and LastPing receives a signed webhook per run. See Monitor a CI/CD pipeline.
Full provider documentation, every attribute and validation rule: registry.terraform.io/providers/lastping-dev/lastping ↗. A dense, agent-readable version of this page lives at lastping.dev/terraform.md ↗.
Reference

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.

Interactive reference: Open the interactive API reference ↗ — every endpoint, schema, and example, generated from the OpenAPI spec (also at /openapi.yaml).

Main resources

MethodPathWhat it does
GET/api/v1/whoamiVerify the key; returns your project id.
POST/api/v1/checksCreate a check (upsert by slug).
GET/api/v1/checksList 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}/pausePause a check (stop alerting).
POST/api/v1/checks/{id}/resumeResume a paused check.
POST/api/v1/channelsCreate a notification channel (webhook or telegram kind).
GET/api/v1/channelsList channels.
PUT/api/v1/checks/{id}/routes/{event_type}Route an event type to channels.
GET/api/v1/checks/{id}/pingsList recent pings.
GET/api/v1/checks/{id}/incidentsList incidents.

Event types for routes are down, recovery, and fail. Channel secrets (webhook secret, Telegram bot_token) are write-only and never returned.

Reference

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"] }'
Need the exact request/response shapes? The interactive API reference is generated from /openapi.yaml.