Developer guide

Trigger your own alerts from anything

Call attn from a cron job, a deploy pipeline, a shell script, or an AI agent. Sign in, connect a destination, mint a key, and POST.

Quick start

From zero to your first alert
  1. Sign in. attn shares your pastebin.ca account — there is nothing new to register. Sign in and open your dashboard →
  2. Connect a destination first. Pair a Telegram chat or enable browser notifications so your test has somewhere to land.
  3. Mint an API key with the attn:notify scope. Copy it immediately — the secret is shown once.
  4. Send your first notification with the call below.
curl -X POST https://attn.ca/api/v1/notify \
  -H "Authorization: Bearer pbca_live_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello from attn","body":"Your first notification worked."}'

Expect 202 Accepted with the notification id. Every verified device you own gets pinged.

{
  "id": "ntfy_3kQ…",
  "status": "accepted",
  "duplicate": false,
  "channels": 2,
  "suppressed": false
}

Recipes

Windows — PowerShell

No curl quoting to fight with cmd.exe or PowerShell’s own escaping rules: -Form submits a hashtable as multipart/form-data for you.

$headers = @{ Authorization = "******" }
$form = @{ title = "Backup failed"; body = "disk full on $(hostname)" }
Invoke-RestMethod -Method Post -Uri "https://attn.ca/api/v1/notify" -Headers $headers -Form $form
Python — any OS, standard library only

No pip install needed — urllib ships with Python. Set the content type explicitly; unlike some HTTP libraries, urllib won’t infer it for you.

import urllib.request
from urllib.parse import urlencode

data = urlencode({"title": "Backup failed", "body": "disk full"}).encode()
req = urllib.request.Request(
    "https://attn.ca/api/v1/notify",
    data=data,
    headers={
        "Authorization": "******",
        "Content-Type": "application/x-www-form-urlencoded",
    },
)
urllib.request.urlopen(req)
Node.js — any OS, no dependencies (Node 18+)

Built-in fetch — passing a URLSearchParams as the body sets the right content type automatically.

await fetch("https://attn.ca/api/v1/notify", {
  method: "POST",
  headers: { Authorization: "******" },
  body: new URLSearchParams({ title: "Backup failed", body: "disk full" }),
});
Cron — only notify on failure

The || means the curl only runs if the job before it exits non-zero — a healthy night stays silent. Swap ****** for your own key.

# crontab -e
0 2 * * * /usr/local/bin/nightly-backup.sh || curl -fsS -X POST https://attn.ca/api/v1/notify \
  -H "Authorization: ******" -H "Content-Type: application/json" \
  -d "{\"title\":\"Backup failed\",\"body\":\"$(hostname) at $(date -Iseconds)\"}"
GitHub Actions — notify on workflow failure

Add as a final step with if: failure(). Store the key as a repo secret and reference it in the Authorization header.

- name: Notify attn on failure
  if: failure()
  run: |
    curl -X POST https://attn.ca/api/v1/notify \
      -H "Authorization: ******" \
      -H "Content-Type: application/json" \
      -d '{"title":"CI failed","body":"workflow failed"}'
systemd — OnFailure= hook for any unit

A reusable notifier unit, wired up with systemd’s own failure-handling hook — no polling, no wrapper script around the real job.

# /etc/systemd/system/attn-notify@.service
[Unit]
Description=Ping attn.ca when %i fails

[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS -X POST https://attn.ca/api/v1/notify \
  -H "Authorization: ******" \
  -d '{"title":"Unit failed","body":"%i failed on %H"}'

Then point any unit you want watched at it: OnFailure=attn-notify@%n.service

Agents — Claude Code, Codex, and friends

Point an MCP-capable agent at attn and it can pair its own destination and send you a ping when it needs you — see Agents & MCP for the server config and full tool list.

How it works

The whole model in five nouns
  • Account — your pastebin.ca identity, shared across the family. No separate signup.
  • Channel — a destination you own and verify: Telegram, a browser (Web Push), or SMS. You can pair several; every verified one receives.
  • API key — an audience-bound bearer token (shape pbca_live_…) scoped to attn and to this site only.
  • Scope — what a key may do: attn:notify (send), attn:read (read activity/status), attn:channels (manage channels), attn:delete (redact settled notification history).
  • Fan-out — one accepted notification is delivered to all your verified channels, retried per channel, at-least-once.
Self-notify onlyA notification can only reach channels the caller has paired and verified. There is no recipient field and no way to message anyone else.

REST API

Authenticate

Send your key as a bearer token. Keys are issued by the pastebin.ca identity authority, audience-bound to https://attn.ca. Mint one from your dashboard.

Full machine-readable spec: openapi.json.

POST /api/v1/notify — send a notification

Scope attn:notify. Body (JSON or form): title, body, priority (low|normal|high, display-only), and an optional dedup_key for safe retries. At least one of title/body is required. Limits: title 256 characters, body 4000, dedup key 200; rate limit 60/minute and 1000/day per account. Dedup keys are account-scoped.

StatusMeaning
202Accepted; returns the notification id. A repeated dedup_key returns the original id without resending.
400Missing title and body.
401Missing or invalid key.
403Key lacks the attn:notify scope, or is bound to another site.
429Rate limited.
curl -X POST https://attn.ca/api/v1/notify \
  -H "Authorization: Bearer pbca_live_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"Backup done","body":"42 GB archived","dedup_key":"backup-2024-06-01"}'

Prefer not to hand-write JSON in a shell script? Send the same fields form-encoded instead — no quoting to get right, no Content-Type header to set (this is curl’s own default for -d):

curl -X POST https://attn.ca/api/v1/notify \
  -H "Authorization: Bearer pbca_live_..." \
  -d "title=Backup done" \
  -d "body=42 GB archived" \
  -d "dedup_key=backup-2024-06-01"

Query-string parameters are deliberately not accepted for this endpoint — title/body would then be logged in plain sight everywhere a URL is (server/proxy access logs, browser history), which a personal alert pipe should not do to its own message content. See Recipes for ready-to-paste examples in PowerShell, Python, and Node.js too.

GET /api/v1/notifications — read recent activity

Scope attn:read. Returns your recent notifications, per-channel delivery status, and summary stats. limit defaults to 25 and is capped at 100.

curl https://attn.ca/api/v1/notifications \
  -H "Authorization: Bearer pbca_live_..."
GET /api/v1/dnd — check Do Not Disturb

Scope attn:read. Reports whether delivery is currently silenced and until when. Enabling or clearing DND is a signed-in dashboard action (bounded to 8 hours; it always expires on its own).

{ "until": 1717200000000, "active": true }
GET /api/v1/credit — SMS credit balance & activity

Scope attn:read. Your remaining SMS credit (in segments, plus an informational USD estimate) and the last 20 ledger movements (grants, charges, refunds, deposits).

curl https://attn.ca/api/v1/credit \
  -H "Authorization: ******"
POST /api/v1/billing/checkout — buy SMS credit

Scope attn:channels for API-key callers; signed-in dashboard sessions are also accepted. Body { "amount_cents": number } ($5–$500, 20 segments per dollar) creates a hosted Stripe Checkout session and returns its URL to redirect to. Credit lands automatically once Stripe confirms payment — see the Billing tab to buy credit interactively.

StatusMeaning
200Returns { "checkout_url": string } to redirect the browser to.
400Amount outside the allowed range.
429Rate limited.
503Stripe is not configured on this environment.
SMS segment cap — GET/POST /api/v1/sms/settings

Scope attn:read (GET) / attn:channels (POST). Caps how many segments a single SMS may split into (1–3, default 3) — longer text is truncated rather than costing extra segments. POST body { "sms_max_segments": number | null } (null resets to the platform default).

{ "sms_max_segments": 1, "platform_max_segments": 3, "is_default": false }
Notification history — settings and deletion

GET /api/v1/notification-settings uses attn:read. Retention defaults to 90 days and can be set in the dashboard to 7, 30, 90, 365 days. DELETE /api/v1/notifications uses attn:delete and redacts settled title/body/dedup content; delivery outcomes and SMS billing metadata remain. In-flight notifications are left unchanged and can be deleted after they settle.

Channels

Pair destinations you own

Pair and manage up to 10 channels from your dashboard or with an attn:channels API key:

  • Telegram — open the bot deep-link and press Start. The chat is verified on the bot’s reply.
  • Web Push — click Enable browser notifications and accept the permission prompt. Pair each browser/device you want pinged.
  • SMS preview — verify a US/Canada (+1) mobile you own with a one-time code. SMS runs on prepaid credit (in segments): buy it yourself from the Billing tab, or an operator can grant it; you are never auto-charged.

REST routes are documented in OpenAPI: list/delete channels, Telegram pair + owner-scoped completion status, Web Push registration/public key, and SMS start/verify. Only verified channels you own ever receive. Agents can pair Telegram (pair_telegram), verify SMS (start_sms_verification / verify_sms), and check SMS credit (get_credit) via MCP tools.

On iPhone & iPadAdd attn.ca to your Home Screen first — iOS only delivers Web Push to installed web apps.

Agents & MCP

A first-class Model Context Protocol server

attn speaks the Model Context Protocol so AI agents can send you alerts and manage your channels. Point an MCP client at the endpoint below and authenticate with an attn API key (bearer) or an OAuth token from the pastebin.ca authority. The tools a client may call depend on the scopes of the key it presents.

ToolScopeWhat it does
whoaminoneEcho account id, scopes, audience, and token kind.
send_notificationattn:notifySend yourself a notification to every verified channel.
list_channelsattn:readList the caller's channels (no secret config).
list_notificationsattn:readList the caller's recent notifications.
pair_telegramattn:channelsStart Telegram pairing; returns a deep-link.
delete_channelattn:channelsRemove one of the caller's channels by id.
delete_notification_historyattn:deleteRedact settled notification content while retaining delivery and billing metadata.
get_creditattn:readShow remaining SMS credit (segments) and recent activity.
start_sms_verificationattn:channelsText a one-time code to an SMS number you own.
verify_smsattn:channelsConfirm the SMS code and verify the channel.
get_sms_settingsattn:readShow your SMS segment cap (max parts a single SMS may split into).
set_sms_max_segmentsattn:channelsSet (or clear) your SMS segment cap, 1-3.

Supported scopes: attn:notify, attn:read, attn:channels, attn:delete.

Connect a client

Most MCP clients take a server URL (and an optional bearer header). Example client config:

{
  "mcpServers": {
    "attn": {
      "url": "https://attn.ca/mcp",
      "headers": { "Authorization": "Bearer pbca_live_…" }
    }
  }
}

Discovery metadata is published for clients that auto-configure:

https://attn.ca/.well-known/agent.json
https://attn.ca/.well-known/oauth-protected-resource/mcp

OAuth & security

Audience-bound keys, OAuth, and DPoP
  • Audience-bound. A key is scoped to one site. An attn key only works on attn.ca; it is rejected everywhere else.
  • OAuth for agents. The MCP endpoint is an OAuth protected resource. Tokens are issued by the pastebin.ca authority and may be DPoP-bound (sender-constrained) so a stolen token can’t be replayed.
  • Revoke instantly. Revoke a key from your dashboard and anything using it stops immediately.