Send a message, alert, or reminder in one HTTP call
One endpoint. One Bearer token. Messages reach humans in a real-time chat UI and on their phone via web push — from a server, website, CLI, cron job, or plugin.
https://pingwire.dev/api/v1/messages.php with an Authorization: Bearer <api_key> header and a JSON body like
{"channel":"deploys","text":"Build #123 passed"}. You get a 201 and the message appears instantly everywhere.
Quickstart — your first message in 30 seconds
Pick your language. Every example sends "Build #123 passed" to a channel called deploys (auto-created on first send).
curl -X POST https://pingwire.dev/api/v1/messages.php \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"channel":"deploys","text":"Build #123 passed","priority":"high"}'#!/usr/bin/env bash
PINGWIRE_KEY="pw_live_xxxxxxxxxxxxxxxxxxxxxxxx"
curl -sS -X POST https://pingwire.dev/api/v1/messages.php \
-H "Authorization: Bearer $PINGWIRE_KEY" \
-H "Content-Type: application/json" \
-d '{"channel":"deploys","text":"Build #123 passed"}'import requests
requests.post(
"https://pingwire.dev/api/v1/messages.php",
headers={"Authorization": "Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx"},
json={"channel": "deploys", "text": "Build #123 passed", "priority": "high"},
)await fetch("https://pingwire.dev/api/v1/messages.php", {
method: "POST",
headers: {
"Authorization": "Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({ channel: "deploys", text: "Build #123 passed" }),
});<?php
$ch = curl_init("https://pingwire.dev/api/v1/messages.php");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["channel" => "deploys", "text" => "Build #123 passed"]),
]);
curl_exec($ch);The send endpoint
| POST | /api/v1/messages.php | Send a message (the core endpoint) |
| POST | /api/v1/notify.php | Identical alias for one-liners |
| GET | /api/v1/messages.php?channel=deploys | List recent messages (paginated) |
How do I send to a person instead of a channel?
Use to with a username or a conversation UUID instead of channel:
curl -X POST https://pingwire.dev/api/v1/messages.php \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{"to":"alice","text":"Your report is ready"}'# Whole request body becomes the message. Channel rides in the query string.
curl -X POST "https://pingwire.dev/api/v1/notify.php?channel=alerts" \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: text/plain" \
--data "Disk usage at 92% on web-01"Message fields
| Field | Type | Description |
|---|---|---|
channel | string | Target channel slug. Auto-created if new. Use this or to. |
to | string | Username or conversation UUID for a direct message. |
text | string | The message body. Up to 16 KB. |
title | string | Optional bold headline (great for alerts). |
priority | enum | min · low · default · high · urgent |
tags | string | Comma-separated labels, e.g. ci,rocket. |
url | string | Click-through URL shown as a button and opened from push. |
Priority levels
Priority controls how loudly a message arrives. urgent pushes with require-interaction so the notification stays until acknowledged.
min low default high urgent
Incoming webhooks — for GitHub, CI & monitors
Create a webhook for a channel in Account → Webhooks. Then POST JSON, form data, or raw text to its URL. No auth header needed — the token is the secret. Pingwire maps common fields (text, message, content, body, title) automatically.
curl -X POST https://pingwire.dev/hook/your_webhook_token \
-H "Content-Type: application/json" \
-d '{"title":"Deploy","text":"v1.2.3 shipped to prod","priority":"high"}'echo "Nightly backup complete" | \
curl -X POST https://pingwire.dev/hook/your_webhook_token \
-H "Content-Type: text/plain" --data-binary @-The pingwire CLI
Install once, then send from any shell, cron job, or pipeline. The bundled PHP CLI works everywhere today; npm, pip and Composer packages are on their way to the registries — until they land, plain HTTP or the bundled CLI is the supported path.
Exit codes are meaningful, so a cron job can act on them:
0 ok · 1 API or usage error ·
2 no credentials · 3 network/timeout (safe to retry).
# Self-hosting or on the server: symlink the bundled wrapper.
chmod +x cli/pingwire
sudo ln -s /home/pingwire.dev/public_html/cli/pingwire /usr/local/bin/pingwire
# Configure (or set PINGWIRE_API_KEY in your env):
echo 'PINGWIRE_API_KEY=pw_live_xxxxxxxxxxxxxxxxxxxxxxxx' >> ~/.pingwirercpingwire send --channel deploys "Build #123 passed"
pingwire send --channel deploys < build.log # pipe stdin
pingwire send --to alice --title "FYI" "ping"
pingwire remind "+10 minutes" "stand up" --channel team
pingwire remind --cron "0 9 * * 1-5" --channel team "Standup"
pingwire channels
pingwire whoamiScheduled & recurring reminders
| POST | /api/v1/reminders.php | Create a one-off or recurring reminder |
| GET | /api/v1/reminders.php | List your reminders |
| DELETE | /api/v1/reminders.php?id=N | Cancel a reminder |
curl -X POST https://pingwire.dev/api/v1/reminders.php \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{"channel":"team","text":"Release window","run_at":"2026-06-01T15:00:00Z"}'curl -X POST https://pingwire.dev/api/v1/reminders.php \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{"channel":"team","text":"Standup","cron":"0 9 * * 1-5","timezone":"America/New_York"}'Requires a key with the schedule scope. Recurring schedules use standard cron expressions.
Channels & identity
| GET | /api/v1/channels.php | List channels you can post to |
| POST | /api/v1/channels.php | Create a channel — {"name":"Deploys"} |
| GET | /api/v1/me.php | Identity for the current key (pingwire whoami) |
Sending to your own username with to lands in a private
“Notes to self” thread and pushes your own devices — a personal reminder lane with no second account.
Identical text from the same machine sender within 60 seconds is collapsed into one message, so a retrying
script cannot spam a channel; human, scheduled, and monitor messages are never deduped.
Message templates
Named, versioned templates with {{variables}}. Editing writes a new immutable version, so a send can always be traced to the exact copy it used.
| GET | /api/v1/templates.php | List your templates (?id=N for one, with all versions) |
| POST | /api/v1/templates.php | Create — {"name":"Deploy done","body":"{{service}} → {{env}}"} |
| POST | /api/v1/templates.php | Preview — {"id":N,"action":"preview","variables":{...}} (renders, saves nothing) |
| POST | /api/v1/templates.php | Archive / unarchive — {"id":N,"action":"archive"} |
Reads need the read scope, writes need send.
Monitors & incidents
Uptime, heartbeat, and traffic monitors feed one incident engine — see the uptime, heartbeat, and traffic pages for how each type behaves.
| GET | /api/v1/monitors | List monitors |
| GET | /api/v1/monitors/{id} | Detail + recent checks |
| POST | /api/v1/monitors | Create a monitor |
| POST | /api/v1/monitors/{id}/pause | Pause (and /resume) |
| GET | /api/v1/incidents | List incidents |
| POST | /api/v1/incidents/{id}/acknowledge | Acknowledge an incident |
Heartbeat check-ins are plain GETs to /hb/<token> (with /start, /fail, or an exit code); the traffic beacon is /t/<token>.gif or a POST to /api/v1/traffic/<token>. Reads need read, writes need send.
Escalation policies
An ordered chain of steps — notify a person or channel, wait, then escalate — that runs when an incident goes unacknowledged.
| GET | /api/v1/escalations.php | List your policies (steps inlined; ?id=N for one) |
| POST | /api/v1/escalations.php | Create — {"name":"Prod","steps":[...]} |
| POST | /api/v1/escalations.php | Replace steps — {"id":N,"steps":[...]} (steps are replaced wholesale, never patched) |
| POST | /api/v1/escalations.php | Deactivate — {"id":N,"action":"archive"} |
Public status pages
Publish your monitors' current state and history at a shareable link — https://pingwire.dev/status/<token>. Rotate the token any time to revoke a shared link.
| GET | /api/v1/status-pages.php | List your pages with public URLs (?id=N for one, with components) |
| POST | /api/v1/status-pages.php | Create — {"title":"Acme status"} |
| POST | /api/v1/status-pages.php | Set components — {"id":N,"monitors":[...]} |
| POST | /api/v1/status-pages.php | Rotate / disable — {"id":N,"action":"rotate"} |
QR Channels & subscriber segments
QR Channels let anyone subscribe to your pushes by scanning a code — no account or app. Read the full overview. Segments group a channel's subscribers so a send can target a slice instead of everyone.
| POST | /api/v1/channels.php | Create a QR channel — {"qr":true,"name":"Deploys"} |
| GET | /api/v1/channels/{id}/subscribers | List a channel's subscribers |
| GET | /api/v1/channels/{id}/qr.png | QR image (also .svg) |
| GET | /api/v1/segments.php?channel=N | List segments with live member counts |
| POST | /api/v1/segments.php | Create — {"channel":N,"name":"VIP"}; manage with action: add | remove | delete |
Ready-to-use recipes
GitHub Actions — notify on deploy
# .github/workflows/notify.yml
name: Notify Pingwire
on: [deployment_status]
jobs:
ping:
runs-on: ubuntu-latest
steps:
- name: Send Pingwire alert
run: |
curl -X POST https://pingwire.dev/hook/${{ secrets.PINGWIRE_HOOK }} \
-H "Content-Type: application/json" \
-d '{"title":"Deploy","text":"${{ github.repository }} deployed ${{ github.sha }}"}'Cron server alert — disk space watchdog
# Alert if root disk usage exceeds 90%. Runs every 5 minutes.
*/5 * * * * [ $(df / | awk 'NR==2{print +$5}') -gt 90 ] && \
pingwire send --channel alerts --priority urgent "Disk >90% on $(hostname)""Is my site up?" monitor
#!/usr/bin/env bash
URL="https://example.com"
if ! curl -fsS --max-time 10 "$URL" >/dev/null; then
curl -X POST https://pingwire.dev/api/v1/messages.php \
-H "Authorization: Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-d "{\"channel\":\"alerts\",\"title\":\"DOWN\",\"text\":\"$URL is unreachable\",\"priority\":\"urgent\"}"
fiMinimal WordPress plugin
<?php
/* Plugin Name: Pingwire Notifier */
add_action('publish_post', function ($id) {
$post = get_post($id);
wp_remote_post('https://pingwire.dev/api/v1/messages.php', [
'headers' => ['Authorization' => 'Bearer pw_live_xxxxxxxxxxxxxxxxxxxxxxxx', 'Content-Type' => 'application/json'],
'body' => wp_json_encode([
'channel' => 'blog',
'title' => 'New post published',
'text' => $post->post_title,
'url' => get_permalink($id),
]),
]);
});Scopes & rate limits
| Scope | Allows |
|---|---|
send | Send messages |
schedule | Create reminders |
read | List messages & channels, identify |
admin | Account-wide automation (admins only) |
- Sends: 120 requests/min per key.
- Webhooks: 60 requests/min per token.
- Reminders: 60 creates/min per key.
- Exceeding a limit returns
429with aRetry-Afterheader.
Besides API keys, Pingwire is an OAuth 2.1 provider (PKCE, rotating refresh tokens): integrations like the Claude MCP connector obtain scoped tokens through user consent, and those tokens work against this same API. AI assistants can drive Pingwire directly through the MCP connector.
Errors
Errors are JSON with a stable error code and the right HTTP status.
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_api_key | Missing/invalid/revoked key |
| 403 | insufficient_scope | Key lacks the required scope |
| 402 | payment_required | The account needs Supporter Access — manage your plan at /account.php#plan |
| 403 | forbidden_channel | Not a member of the target channel |
| 404 | user_not_found | to doesn't match any username (human-readable detail included) |
| 422 | missing_target | No channel or to provided |
| 429 | rate_limited | Slow down — see Retry-After |
llms.txt documents this API for AI coding assistants too.