Pingwire API

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.

Quick answer: POST to 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.
Tip: Create a free account and your real API key will be injected into these examples automatically.

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.phpSend a message (the core endpoint)
POST/api/v1/notify.phpIdentical alias for one-liners
GET/api/v1/messages.php?channel=deploysList 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

FieldTypeDescription
channelstringTarget channel slug. Auto-created if new. Use this or to.
tostringUsername or conversation UUID for a direct message.
textstringThe message body. Up to 16 KB.
titlestringOptional bold headline (great for alerts).
priorityenummin · low · default · high · urgent
tagsstringComma-separated labels, e.g. ci,rocket.
urlstringClick-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' >> ~/.pingwirerc
pingwire 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 whoami

Scheduled & recurring reminders

POST/api/v1/reminders.phpCreate a one-off or recurring reminder
GET/api/v1/reminders.phpList your reminders
DELETE/api/v1/reminders.php?id=NCancel 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.phpList channels you can post to
POST/api/v1/channels.phpCreate a channel — {"name":"Deploys"}
GET/api/v1/me.phpIdentity 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.phpList your templates (?id=N for one, with all versions)
POST/api/v1/templates.phpCreate — {"name":"Deploy done","body":"{{service}} → {{env}}"}
POST/api/v1/templates.phpPreview — {"id":N,"action":"preview","variables":{...}} (renders, saves nothing)
POST/api/v1/templates.phpArchive / 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/monitorsList monitors
GET/api/v1/monitors/{id}Detail + recent checks
POST/api/v1/monitorsCreate a monitor
POST/api/v1/monitors/{id}/pausePause (and /resume)
GET/api/v1/incidentsList incidents
POST/api/v1/incidents/{id}/acknowledgeAcknowledge 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.phpList your policies (steps inlined; ?id=N for one)
POST/api/v1/escalations.phpCreate — {"name":"Prod","steps":[...]}
POST/api/v1/escalations.phpReplace steps — {"id":N,"steps":[...]} (steps are replaced wholesale, never patched)
POST/api/v1/escalations.phpDeactivate — {"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.phpList your pages with public URLs (?id=N for one, with components)
POST/api/v1/status-pages.phpCreate — {"title":"Acme status"}
POST/api/v1/status-pages.phpSet components — {"id":N,"monitors":[...]}
POST/api/v1/status-pages.phpRotate / 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.phpCreate a QR channel — {"qr":true,"name":"Deploys"}
GET/api/v1/channels/{id}/subscribersList a channel's subscribers
GET/api/v1/channels/{id}/qr.pngQR image (also .svg)
GET/api/v1/segments.php?channel=NList segments with live member counts
POST/api/v1/segments.phpCreate — {"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\"}"
fi

Minimal 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

ScopeAllows
sendSend messages
scheduleCreate reminders
readList messages & channels, identify
adminAccount-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 429 with a Retry-After header.

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.

StatusCodeMeaning
401invalid_api_keyMissing/invalid/revoked key
403insufficient_scopeKey lacks the required scope
402payment_requiredThe account needs Supporter Access — manage your plan at /account.php#plan
403forbidden_channelNot a member of the target channel
404user_not_foundto doesn't match any username (human-readable detail included)
422missing_targetNo channel or to provided
429rate_limitedSlow down — see Retry-After

llms.txt documents this API for AI coding assistants too.