Ping

Ping Documentation

Webhook-to-push notifications for developers. POST JSON to a URL, get a notification on your phone.


Table of Contents


Introduction

Ping is a simple webhook-to-push-notification service designed for developers. It bridges the gap between your automated systems and your phone, ensuring you never miss important events.

Use Cases

  • CI/CD Pipelines - Get notified when builds complete or fail
  • Server Monitoring - Receive alerts for high CPU, memory, or disk usage
  • Workflow Automation - n8n, Zapier, or custom automation notifications
  • Cron Jobs - Know when scheduled tasks complete
  • AI Agents - Get updates from long-running AI processes
  • E-commerce - New order notifications
  • Custom Applications - Any system that can make HTTP requests

How It Works

Your System
POST
Ping API
APNs
Your Phone
  1. Your system POSTs JSON to your webhook URL
  2. Ping stores the message and sends it via Apple Push Notifications
  3. You receive an instant notification on your iPhone

Getting Started

Installation

  1. Download Ping from TestFlight (App Store release coming soon)
  2. Open the app on your iPhone

Sign In

Ping uses Sign in with Apple for secure authentication. This enables:

  • Secure, passwordless authentication
  • Cross-device sync of channels and messages
  • Privacy-focused login (hide your email if desired)

Tap Sign in with Apple and authenticate with Face ID, Touch ID, or your passcode.

Enable Notifications

When prompted, tap Allow to enable push notifications. This is required for Ping to work.

If you accidentally denied notifications:

  1. Open Settings on your iPhone
  2. Scroll to Ping
  3. Tap Notifications
  4. Enable Allow Notifications

Create Your First Channel

Channels are the core concept in Ping. Each channel has its own webhook URL and groups related notifications together.

  1. Tap the Channels tab
  2. Tap the + button
  3. Enter a name (e.g., "Server Alerts")
  4. Choose an icon and color
  5. Tap Create

Send Your First Notification

Copy your webhook URL and send a test notification:

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Hello from Ping!"}'

Preview

Hello from Ping!

just now

You should receive a push notification within seconds.


Channels

What is a Channel?

A channel is a notification endpoint. Each channel has:

  • Name - A descriptive label (e.g., "Deploy Alerts")
  • Icon - An SF Symbol for visual identification
  • Color - A color for the channel badge
  • Webhook URL - A unique URL for sending notifications
  • Messages - A history of all notifications sent to this channel

Think of channels like chat groups - each one collects related notifications together.

Creating Channels

  1. Open the Channels tab
  2. Tap the + button
  3. Fill in the details:
    • Name: Something descriptive
    • Icon: Choose from available SF Symbols
    • Color: Pick a color for visual identification
  4. Tap Create

Your new channel appears with a unique webhook URL.

Suggested Channels:

  • Deploy Alerts - CI/CD notifications
  • Server Monitoring - Infrastructure alerts
  • n8n Workflows - Automation updates
  • Cron Jobs - Scheduled task completion
  • Error Logs - Application errors

Managing Channels

Viewing Channel Details

Tap any channel to see:

  • Message history (newest first)
  • Webhook URL
  • Channel settings

Editing a Channel

  1. Tap the channel
  2. Tap the settings icon
  3. Modify name, icon, or color
  4. Tap Save

Deleting a Channel

  1. Swipe left on the channel in the list
  2. Tap Delete

Warning: Deleting a channel also deletes all its messages. This cannot be undone.

Channel Muting

Temporarily silence push notifications from a channel while still collecting messages. This is useful when you want to avoid interruptions but don't want to miss data.

Muting a Channel

  1. Tap the channel
  2. Tap Settings
  3. Tap Mute Channel
  4. Select a duration (1 hour, 8 hours, 24 hours, or custom)

How Muting Works

BehaviorMutedActive
Messages stored in database
Push notification sent
Appears in message history
Webhook returns muted: true

When muted, the channel badge shows a muted indicator. Messages continue to be stored and appear in your message history - only push notifications are suppressed.

Via API

Update a channel's muted_until field via the API:

Terminal
curl -X PATCH 'https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"muted_until": "2026-01-20T00:00:00Z"}'

Set muted_until to null to unmute immediately:

Terminal
curl -X PATCH 'https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"muted_until": null}'

Webhook URLs

Your webhook URL looks like this:

https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN

The token at the end is your authentication. Keep this URL private - anyone with access can send you notifications.

Copying the URL

  1. Tap the channel
  2. Tap the webhook URL to copy it
  3. Paste it in your integration

Regenerating Tokens

If your webhook URL is compromised:

  1. Tap the channel
  2. Tap Settings
  3. Tap Regenerate Token
  4. Confirm the action

Your old URL stops working immediately. Update any integrations with the new URL.


Webhooks

Sending Notifications

Send a POST request to your webhook URL with a JSON body:

Terminal
curl -X POST 'https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Hello World"}'

Preview

Hello World

just now

Payload Reference

FieldTypeRequiredDescriptionConstraints
titlestringYesNotification titleMax 100 characters
bodystringNoNotification body (supports Markdown)Max 10,000 characters
prioritystringNoDelivery prioritylow, normal, high
callback_urlstringConditionalURL to receive action responsesRequired for interactive actions
actionsarrayNoInteractive actionsMax 5 actions

Markdown Support

The body field supports Markdown formatting, which is rendered beautifully in the iOS app. This is ideal for AI-generated content, reports, and structured messages.

Supported formatting:

  • Bold: **text** or __text__
  • Italic: *text* or _text_
  • Inline code: `code`
  • Links: [label](url)

Example: Traffic Report with Markdown

JSON
{
  "title": "🚗 Traffic Update",
  "body": "**⚽ EVENTS & SCHEDULED DISRUPTIONS**\n\n🔴 **Southampton vs Leicester City** - Tuesday, 18 Nov **in 3 days**\n\nSt. Mary's Stadium\n- Peak congestion: 18:30-20:30 before, 22:00-23:00 after\n- Most affected: *Britannia Road*, *St Mary's Road*\n\n---\n\n**🚢 CRUISE TERMINAL**\n\n🟡 **Queen Mary 2** - Saturday **in 5 days**\n- Arrival: 06:00, Departure: 17:00"
}

Preview

🚗 Traffic Update

just now
⚽ EVENTS & SCHEDULED DISRUPTIONS 🔴 Southampton vs Leicester City - Tuesday, 18 Nov in 3 days St. Mary's Stadium - Peak congestion: 18:30-20:30 before, 22:00-23:00 after - Most affected: Britannia Road, St Mary's Road --- 🚢 CRUISE TERMINAL 🟡 Queen Mary 2 - Saturday in 5 days - Arrival: 06:00, Departure: 17:00

This renders with proper bold headings, italic text, and clean formatting in the app - perfect for AI-generated summaries from tools like Perplexity, ChatGPT, or n8n workflows.

Full Example

JSON
{
  "title": "Build Failed",
  "body": "CI pipeline failed on main branch at commit abc123",
  "priority": "high",
  "actions": [
    {"type": "url", "label": "View Build", "value": "https://github.com/org/repo/actions/runs/123"},
    {"type": "url", "label": "View Logs", "value": "https://ci.example.com/logs/123"}
  ]
}

Preview

Build Failed

just now
CI pipeline failed on main branch at commit abc123

Interactive Example with Callback

JSON
{
  "title": "Approve Request",
  "body": "John requested access to the production database",
  "callback_url": "https://your-server.com/webhook/response",
  "actions": [
    {"type": "button", "label": "Approve", "key": "approve", "style": "primary"},
    {"type": "button", "label": "Reject", "key": "reject", "style": "destructive"}
  ]
}

Preview

Approve Request

just now
John requested access to the production database

Priority Levels

PriorityBehavior
highPlays a sound, shows alert indicator
normalStandard notification (default)
lowLower priority delivery

Use high priority sparingly for truly urgent alerts. Overusing it reduces its effectiveness.

Actions

Add interactive elements to your notifications. There are two categories:

  1. URL actions - Open links in Safari (no callback needed)
  2. Interactive actions - Collect user input and POST responses to your callback URL

Action Types

TypeDescriptionRequires keyRequires callback_url
urlOpens external URLNoNo
buttonClickable buttonOptionalYes
selectDropdown menuYesYes
textText input fieldYesYes
numberNumeric inputYesYes
toggleOn/off switchYesYes

URL Actions

Open links directly in Safari. No callback URL required.

JSON
{
  "type": "url",
  "label": "View Build",
  "value": "https://github.com/org/repo/actions/runs/123"
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "url"
labelstringYesButton textMax 30 characters
valuestringYesURL to openMax 500 characters, valid URL

Button Actions

Clickable buttons that trigger callbacks. Ideal for approve/reject workflows.

JSON
{
  "type": "button",
  "label": "Approve",
  "key": "approve",
  "style": "primary"
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "button"
labelstringYesButton textMax 30 characters
keystringNoIdentifier for callbackMax 50 characters
stylestringNoVisual styleprimary, secondary, destructive

Select Actions

Dropdown menu for selecting from predefined options.

JSON
{
  "type": "select",
  "label": "Priority",
  "key": "priority",
  "options": [
    {"label": "Low", "value": "low"},
    {"label": "Medium", "value": "medium"},
    {"label": "High", "value": "high"}
  ],
  "value": "medium"
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "select"
labelstringYesLabel textMax 30 characters
keystringYesIdentifier for callbackMax 50 characters
optionsarrayYesSelection optionsMax 10 options
options[].labelstringYesDisplay textMax 50 characters
options[].valuestringYesValue sent in callbackMax 100 characters
valuestringNoDefault selection

Text Input Actions

Free-form text entry field.

JSON
{
  "type": "text",
  "label": "Reply",
  "key": "message",
  "placeholder": "Type your message...",
  "multiline": true
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "text"
labelstringYesLabel textMax 30 characters
keystringYesIdentifier for callbackMax 50 characters
placeholderstringNoPlaceholder textMax 100 characters
multilinebooleanNoAllow multiple linesDefault: false
valuestringNoDefault text

Number Input Actions

Numeric input with optional min/max bounds.

JSON
{
  "type": "number",
  "label": "Quantity",
  "key": "quantity",
  "min": 1,
  "max": 100,
  "defaultValue": 10
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "number"
labelstringYesLabel textMax 30 characters
keystringYesIdentifier for callbackMax 50 characters
minnumberNoMinimum value
maxnumberNoMaximum value
defaultValuenumberNoDefault value

Toggle Actions

Boolean on/off switch.

JSON
{
  "type": "toggle",
  "label": "Enable notifications",
  "key": "enabled",
  "value": "false"
}
FieldTypeRequiredDescriptionConstraints
typestringYesMust be "toggle"
labelstringYesLabel textMax 30 characters
keystringYesIdentifier for callbackMax 50 characters
valuestringNoDefault state"true" or "false"

Callback URL

When using interactive actions (button, select, text, number, toggle), you can choose how to receive the user's response:

  1. Callback URL - Provide a callback_url and the iOS app POSTs the response directly to your server
  2. Polling - Skip the callback_url and poll Ping for the response (no external server needed)
Option 1: Callback URL

When you provide a callback_url, the iOS app POSTs the response directly to your server when the user interacts with an action.

Callback Request Format:

JSON
{
  "message_id": "msg_xxxxxxxxxxxx",
  "channel_id": "ch_xxxxxxxxxxxx",
  "timestamp": "2026-01-12T20:18:13Z",
  "triggered_by": "approve",
  "responses": {
    "rating": "good",
    "comment": "User entered text"
  }
}
FieldDescription
message_idThe message ID that was interacted with
channel_idThe channel the message belongs to
timestampISO8601 timestamp of when the action was triggered
triggered_byThe key of the button/action that was tapped
responsesObject containing all form field values (keyed by field key)

iOS App Behavior:

The iOS app provides an intuitive interface for callbacks:

  • Button-only notifications (no form fields): Buttons appear directly in the message. Tapping immediately sends the callback.
  • Notifications with form fields: Tapping a button opens a sheet where users fill out the form, then tap Submit at the bottom.
  • After submission: Buttons are replaced with a "Submitted" indicator. Tap it to see callback details (status code, timestamp, form values).
  • On failure: A "Failed" indicator appears. Tap it to see the error details.

Example: Approval Workflow

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Approve Request",
    "body": "Alice requested access to production",
    "callback_url": "https://your-server.com/approvals",
    "actions": [
      {"type": "button", "label": "Approve", "key": "approve", "style": "primary"},
      {"type": "button", "label": "Reject", "key": "reject", "style": "destructive"}
    ]
  }'

Preview

Approve Request

just now
Alice requested access to production

Example: Complex Form

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Issue Report",
    "body": "Please provide details",
    "callback_url": "https://your-server.com/issues",
    "actions": [
      {
        "type": "select",
        "label": "Category",
        "key": "category",
        "options": [
          {"label": "Bug", "value": "bug"},
          {"label": "Feature", "value": "feature"}
        ]
      },
      {
        "type": "text",
        "label": "Description",
        "key": "description",
        "placeholder": "Describe the issue...",
        "multiline": true
      },
      {"type": "button", "label": "Submit", "key": "submit", "style": "primary"}
    ]
  }'

Preview

Issue Report

just now
Please provide details
Option 2: Polling (No Server Required)

For simple approval workflows, you can skip the callback_url entirely. When the user taps a button or submits a form, their response is stored in Ping. Your system can then poll for the response using your webhook token.

This is the "Ping it Back" pattern - send a notification, get a response, all without running a server.

Ideal for:

  • CLI tools waiting for user approval
  • AI agents (like Claude Code) that need human confirmation
  • CI/CD pipelines needing deployment approval
  • n8n/Zapier workflows with human-in-the-loop decisions
  • Any script that needs mobile user input

How it works:

Your Script                    Ping API                     Your Phone
    │                              │                             │
    │  POST /v1/send/{token}       │                             │
    │  (with action buttons)       │                             │
    │ ────────────────────────────>│   Push Notification         │
    │                              │ ───────────────────────────>│
    │  {"ok": true, "id": "msg_x"} │                             │
    │ <────────────────────────────│                             │
    │                              │                             │
    │  GET /v1/callback/{token}/msg_x                            │
    │ ────────────────────────────>│                             │
    │  {"status": "pending"}       │        User taps button     │
    │ <────────────────────────────│ <───────────────────────────│
    │                              │                             │
    │  GET /v1/callback/{token}/msg_x                            │
    │ ────────────────────────────>│                             │
    │  {"status": "submitted",     │                             │
    │   "result": {"triggered_by": "approve"}}                   │
    │ <────────────────────────────│                             │
    │                              │                             │
    ▼  Continue based on response  │                             │

Example: Simple Approval

Terminal
# 1. Send the notification (no callback_url needed)
RESPONSE=$(curl -s -X POST 'https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Approve Deploy?",
    "body": "Deploy v2.1.0 to production",
    "actions": [
      {"type": "button", "label": "Approve", "key": "approve", "style": "primary"},
      {"type": "button", "label": "Reject", "key": "reject", "style": "destructive"}
    ]
  }')

# Extract message ID
MSG_ID=$(echo $RESPONSE | jq -r '.id')
echo "Waiting for response on message: $MSG_ID"

Preview

Approve Deploy?

just now
Deploy v2.1.0 to production
Terminal
# 2. Poll for the response (uses webhook token, no auth needed)
curl -s "https://ping-api-production.up.railway.app/v1/callback/YOUR_TOKEN/$MSG_ID"

# Before user responds:
# {"status": "pending"}

# After user taps "Approve":
# {"status": "submitted", "result": {"triggered_by": "approve", "responses": {}}, "submitted_at": "..."}

Security Protections:

ProtectionDescription
Single readResponses can only be read once, then they're cleared
Auto-expireResponses expire after 10 minutes if not retrieved
Token authUses your webhook token (no session needed)
No overwriteCannot submit multiple responses to the same message

Polling Response Statuses:

StatusMeaning
pendingUser hasn't responded yet
submittedUser responded - check result field
expiredResponse expired (10-minute TTL)
already_readResponse was already consumed
failedCallback delivery failed

Full Polling Response:

JSON
{
  "status": "submitted",
  "result": {
    "message_id": "msg_xxxxxxxxxxxx",
    "channel_id": "ch_xxxxxxxxxxxx",
    "timestamp": "2026-01-13T15:30:00Z",
    "triggered_by": "approve",
    "responses": {
      "environment": "production",
      "notes": "Looks good!"
    }
  },
  "submitted_at": "2026-01-13T15:30:00Z"
}
FieldDescription
triggered_byThe key of the button that was tapped
responsesForm field values (keyed by field key)
timestampWhen the user submitted their response

Complete Polling Script:

Terminal
#!/bin/bash
# Complete "Ping it Back" approval script

TOKEN="YOUR_WEBHOOK_TOKEN"
API="https://ping-api-production.up.railway.app"

# Send notification with buttons
RESPONSE=$(curl -s -X POST "$API/v1/send/$TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Deploy to Production?",
    "body": "Version 2.1.0 ready for release",
    "priority": "high",
    "actions": [
      {"type": "button", "label": "Deploy", "key": "deploy", "style": "primary"},
      {"type": "button", "label": "Cancel", "key": "cancel", "style": "destructive"}
    ]
  }')

MSG_ID=$(echo $RESPONSE | jq -r '.id')
echo "📱 Notification sent. Waiting for approval..."

# Poll for response (max 5 minutes)
TIMEOUT=300
ELAPSED=0
INTERVAL=3

while [ $ELAPSED -lt $TIMEOUT ]; do
  RESULT=$(curl -s "$API/v1/callback/$TOKEN/$MSG_ID")
  STATUS=$(echo $RESULT | jq -r '.status')

  case $STATUS in
    "submitted")
      ACTION=$(echo $RESULT | jq -r '.result.triggered_by')
      if [ "$ACTION" = "deploy" ]; then
        echo "✅ Approved! Starting deployment..."
        # Your deploy command here
        exit 0
      else
        echo "❌ Cancelled by user"
        exit 1
      fi
      ;;
    "expired"|"already_read")
      echo "⏰ Response expired or already used"
      exit 1
      ;;
    "pending")
      sleep $INTERVAL
      ELAPSED=$((ELAPSED + INTERVAL))
      ;;
  esac
done

echo "⏰ Timeout waiting for approval"
exit 1

Preview

Deploy to Production?

just now
Version 2.1.0 ready for release

Constraints

  • Maximum 5 actions per message
  • Label max 30 characters
  • Key max 50 characters
  • URL max 500 characters (must be valid)
  • Select options max 10 options
  • Option label max 50 characters
  • Option value max 100 characters
  • Placeholder max 100 characters
  • callback_url is optional - use it for server-to-server callbacks, or skip it and poll for responses
  • callback_url is not needed for URL-only actions

Webhook Signature Verification

Ping signs all outgoing callbacks with HMAC-SHA256 signatures. This allows you to verify that callbacks are genuinely from Ping and haven't been tampered with.

Signature Header

Every callback includes an X-Ping-Signature header:

X-Ping-Signature: t=1768483312,v1=21c1746916fac3c5e98835f5c00ea78081a122a26843fbff05961bd9cf059ec9
  • t - Unix timestamp when the signature was generated
  • v1 - HMAC-SHA256 signature of "timestamp.payload"

Getting Your Webhook Secret

Each channel has a unique secret for signature verification:

Terminal
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID/secret"

Response:

JSON
{
  "webhook_secret": "4140db9a5fa04fe3208183ceb1d659cafbfa23ff3f31e7cc3777fd3062e60992",
  "algorithm": "HMAC-SHA256",
  "header_name": "X-Ping-Signature"
}

Verification Steps

  1. Extract t (timestamp) and v1 (signature) from the header
  2. Check timestamp is within 5 minutes (replay protection)
  3. Reconstruct: "${timestamp}.${rawBody}"
  4. Compute HMAC-SHA256 with your webhook secret
  5. Compare using timing-safe comparison

Quick Verification (Node.js)

const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, secret) {
  const [tPart, sigPart] = signatureHeader.split(',');
  const timestamp = parseInt(tPart.split('=')[1]);
  const signature = sigPart.split('=')[1];

  // Check timestamp (5 min tolerance)
  if (Math.abs(Date.now()/1000 - timestamp) > 300) return false;

  // Verify signature
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Rotating Secrets

If your secret is compromised:

Terminal
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  "https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID/secret/rotate"

Debugging Signatures

Use the public verification endpoints to debug issues:

Terminal
# Generate a test signature
curl -X POST "https://ping-api-production.up.railway.app/v1/verify/generate" \
  -H "Content-Type: application/json" \
  -d '{"payload": "{\"test\":true}", "secret": "YOUR_SECRET"}'

# Verify a signature
curl -X POST "https://ping-api-production.up.railway.app/v1/verify/signature" \
  -H "Content-Type: application/json" \
  -d '{"payload": "...", "signature": "t=...,v1=...", "secret": "YOUR_SECRET"}'

For complete documentation, see the Webhook Signatures Guide.

Response Format

Success

JSON
{
  "ok": true,
  "id": "msg_xxxxxxxxxxxx",
  "muted": false
}
FieldDescription
okAlways true on success
idUnique message identifier for tracking or polling
mutedtrue if the channel is muted (message stored, but no push sent)

The id is a unique identifier for the message. You can use this for tracking, debugging, or polling for callback responses.

Error

JSON
{
  "error": "error_code",
  "message": "Human-readable description"
}

Error Handling

Error CodeHTTP StatusDescriptionSolution
invalid_json400Request body isn't valid JSONCheck JSON syntax
invalid_payload400Missing title, exceeds limits, or missing callback_url for interactive actionsVerify required fields and add callback_url
invalid_token401Channel not foundCheck webhook URL
rate_limited429Too many requestsWait and retry

Common invalid_payload errors:

  • Missing title field
  • callback_url missing when using interactive actions (button, select, text, number, toggle)
  • callback_url is not a valid URL
  • Field exceeds character limits

Rate Limits

Rate limits vary by plan:

PlanWebhook RateOther Endpoints
Free5/min per channel60/min per endpoint
Pro200/min per channel200/min per endpoint

Note: Webhook transformer endpoints (Railway, GitHub, Vercel) share the same limits as the main webhook endpoint.

When rate limited, you'll receive HTTP 429:

JSON
{
  "error": "rate_limited",
  "message": "Too many requests"
}

Tips:

  • Wait before retrying (exponential backoff recommended)
  • Create multiple channels for high-volume use cases
  • Upgrade to Pro for higher limits

Integrations

curl

The simplest way to send notifications from any system with curl installed.

Basic Notification

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Task Complete"}'

Preview

Task Complete

just now

With Body

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Deploy Complete", "body": "v2.1.0 deployed to production"}'

Preview

Deploy Complete

just now
v2.1.0 deployed to production

High Priority Alert

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Server Alert", "body": "CPU usage above 90%", "priority": "high"}'

Preview

Server Alert

just now
CPU usage above 90%

Handling Special Characters

If your message contains special characters, use printf:

Terminal
printf '{"title":"Alert","body":"Something happened: check logs"}' | \
  curl -s -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' -d @-

JavaScript / Node.js

const WEBHOOK_URL = 'YOUR_WEBHOOK_URL';

async function sendNotification(title, body, priority = 'normal') {
  const response = await fetch(WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, body, priority })
  });

  return response.json();
}

// Usage
await sendNotification('Build Complete', 'All tests passed');
await sendNotification('Error', 'Database connection failed', 'high');

With Actions

await fetch(WEBHOOK_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    title: 'PR Review Requested',
    body: 'alice requested your review on #42',
    actions: [
      { type: 'url', label: 'View PR', value: 'https://github.com/org/repo/pull/42' }
    ]
  })
});

Python

import requests

WEBHOOK_URL = 'YOUR_WEBHOOK_URL'

def send_notification(title, body=None, priority='normal'):
    payload = {'title': title, 'priority': priority}
    if body:
        payload['body'] = body

    response = requests.post(WEBHOOK_URL, json=payload)
    return response.json()

# Usage
send_notification('Script Finished', 'Processed 1,000 records')
send_notification('Error', 'Task failed with exit code 1', 'high')

With Error Handling

import requests

def notify(title, body=None, priority='normal'):
    try:
        response = requests.post(
            WEBHOOK_URL,
            json={'title': title, 'body': body, 'priority': priority},
            timeout=10
        )
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f'Failed to send notification: {e}')
        return None

GitHub Actions

Add notifications to your CI/CD workflows.

Workflow File

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        run: ./deploy.sh

      - name: Notify Success
        if: success()
        run: |
          curl -X POST '${{ secrets.PING_WEBHOOK_URL }}' \
            -H 'Content-Type: application/json' \
            -d '{
              "title": "Deploy Complete",
              "body": "${{ github.repository }} deployed",
              "actions": [
                {"type": "url", "label": "View Run", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}
              ]
            }'

      - name: Notify Failure
        if: failure()
        run: |
          curl -X POST '${{ secrets.PING_WEBHOOK_URL }}' \
            -H 'Content-Type: application/json' \
            -d '{
              "title": "Deploy Failed",
              "body": "${{ github.repository }} - ${{ github.ref_name }}",
              "priority": "high",
              "actions": [
                {"type": "url", "label": "View Run", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}
              ]
            }'

Preview

Deploy Complete

just now
${{ github.repository }} deployed

Setup

  1. Go to your repository Settings > Secrets and variables > Actions
  2. Click New repository secret
  3. Name: PING_WEBHOOK_URL
  4. Value: Your webhook URL
  5. Click Add secret

n8n

Use HTTP Request nodes to send notifications and receive responses from your n8n workflows.

Simple Notification

SettingValue
MethodPOST
URLhttps://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN
Body Content TypeJSON
JSON
{
  "title": "Workflow Complete",
  "body": "{{ $json.summary }}",
  "priority": "normal"
}

Preview

Workflow Complete

just now
{{ $json.summary }}

Bidirectional Workflow (Ping it Back)

Create human-in-the-loop workflows where n8n waits for your mobile response:

Workflow Structure:

Trigger → Send Ping → Loop (Poll for Response) → Branch on Result

Step 1: Send Notification with Buttons (HTTP Request)

SettingValue
MethodPOST
URLhttps://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN
JSON
{
  "title": "Approve Purchase Order",
  "body": "{{ $json.vendor }} - ${{ $json.amount }}",
  "priority": "high",
  "actions": [
    {"type": "button", "label": "Approve", "key": "approve", "style": "primary"},
    {"type": "button", "label": "Reject", "key": "reject", "style": "destructive"}
  ]
}

Preview

Approve Purchase Order

just now
{{ $json.vendor }} - ${{ $json.amount }}

Step 2: Loop Node (Poll for Response)

Add a Loop node that polls until a response is received:

SettingValue
Loop TypeWhile
Condition{{ $json.status === 'pending' }}
Max Iterations100
Delay3000ms

Inside the loop, add an HTTP Request:

SettingValue
MethodGET
URLhttps://ping-api-production.up.railway.app/v1/callback/YOUR_TOKEN/{{ $('Send Ping').item.json.id }}

Step 3: Branch on Response (IF Node)

Condition{{ $json.result.triggered_by === 'approve' }}

Then route to "Approved" or "Rejected" branches.

Use Cases:

  • Purchase order approvals
  • Access request workflows
  • Content moderation decisions
  • Escalation confirmations

Dynamic Content

Use n8n expressions to include workflow data:

JSON
{
  "title": "New Lead: {{ $json.name }}",
  "body": "Email: {{ $json.email }}\nCompany: {{ $json.company }}",
  "actions": [
    {"type": "url", "label": "View Lead", "value": "{{ $json.crm_url }}"}
  ]
}

Preview

New Lead: {{ $json.name }}

just now
Email: {{ $json.email }} Company: {{ $json.company }}

Shell Scripts

Create a reusable notification function:

Terminal
#!/bin/bash

PING_URL="${PING_WEBHOOK_URL:-YOUR_WEBHOOK_URL}"

notify() {
  local title="$1"
  local body="$2"
  local priority="${3:-normal}"

  curl -s -X POST "$PING_URL" \
    -H 'Content-Type: application/json' \
    -d "{\"title\": \"$title\", \"body\": \"$body\", \"priority\": \"$priority\"}"
}

# Usage examples
notify "Backup Complete" "Database backed up successfully"
notify "Alert" "Disk space low on /dev/sda1" "high"

# In a script
if ./process_data.sh; then
  notify "Process Complete" "Data processed successfully"
else
  notify "Process Failed" "Check logs for details" "high"
fi

Docker

Send notifications from Docker containers:

Terminal
# Simple one-off notification
docker run --rm curlimages/curl \
  -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Container Started"}'

Preview

Container Started

just now

In Docker Compose

services:
  app:
    image: your-app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  notify:
    image: curlimages/curl
    depends_on:
      - app
    command: >
      -X POST 'YOUR_WEBHOOK_URL'
      -H 'Content-Type: application/json'
      -d '{"title": "Stack Started", "body": "All services are running"}'

Preview

Stack Started

just now
All services are running

Cron Jobs

Notify when scheduled tasks complete:

Terminal
# In crontab (crontab -e)

# Daily backup at 2 AM
0 2 * * * /path/to/backup.sh && curl -s -X POST 'YOUR_WEBHOOK_URL' -H 'Content-Type: application/json' -d '{"title": "Backup Complete"}'

# Weekly report on Sundays
0 9 * * 0 /path/to/report.sh && curl -s -X POST 'YOUR_WEBHOOK_URL' -H 'Content-Type: application/json' -d '{"title": "Weekly Report Generated"}'

# With error notification
0 3 * * * /path/to/job.sh || curl -s -X POST 'YOUR_WEBHOOK_URL' -H 'Content-Type: application/json' -d '{"title": "Cron Job Failed", "priority": "high"}'

Preview

Backup Complete

just now

Claude Code

Integrate Ping with Claude Code to get push notifications when Claude finishes tasks or needs approval for deploy commands.

Use Cases

  • Long-running tasks - Get notified when Claude finishes a big refactor
  • Background agents - Fire off a task and get pinged when it's done
  • Deploy approvals - Approve deployments from your phone before they run
  • Security alerts - Get notified when Claude runs potentially risky commands

Quick Setup

  1. Create a channel for Claude Code notifications in the Ping app
  2. Copy your webhook token
  3. Add to your project's .claude/settings.json:
JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "export PING_WEBHOOK_TOKEN=YOUR_TOKEN && python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/deploy-approval.py\"",
            "timeout": 310
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/ping-notify.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
  1. Copy the hook scripts to your .claude/hooks/ directory (available in the Ping repository)

Deploy Approval Flow

When enabled, the deploy approval hook:

  1. Detects deploy commands (git push, railway up, vercel deploy, etc.)
  2. Sends a notification to your phone with Approve/Reject buttons
  3. Waits up to 5 minutes for your response
  4. If approved: runs the command
  5. If rejected: blocks the command

This uses Ping's callback polling API - no external server or ngrok required.

For detailed documentation, see the Claude Code integration guide.

AI Agents (Markdown API)

Ping documentation is available in raw Markdown format for AI coding assistants like Claude Code, Cursor, Windsurf, and other LLM-powered tools. This enables AI agents to fetch up-to-date documentation directly.

Fetching Documentation

Request markdown by sending the Accept: text/markdown header:

Terminal
curl -H 'Accept: text/markdown' https://getping.pro/docs

This returns the raw markdown content instead of the HTML page.

Claude Code

Add Ping documentation to your Claude Code context:

Terminal
# Read the docs directly into context
curl -sH 'Accept: text/markdown' https://getping.pro/docs | claude --read

Or add to your project's custom instructions in .claude/CLAUDE.md:

## Ping Integration

When working with Ping webhooks, fetch the latest documentation:
- URL: https://getping.pro/docs
- Header: Accept: text/markdown

Cursor

Add as a documentation source in Cursor settings:

  1. Open Cursor Settings > Features > Docs
  2. Click Add new doc
  3. Enter URL: https://getping.pro/docs
  4. Cursor will automatically fetch markdown when available

Windsurf & Other AI Editors

Most AI-powered editors support adding documentation URLs. Configure them to fetch https://getping.pro/docs with the Accept: text/markdown header for raw markdown access.

MCP Servers

If your AI tool supports MCP (Model Context Protocol) servers, you can configure a fetch tool to retrieve the documentation:

JSON
{
  "url": "https://getping.pro/docs",
  "headers": {
    "Accept": "text/markdown"
  }
}

Why Markdown?

  • Smaller payload - Raw markdown is more compact than HTML
  • Better context - AI models understand markdown structure natively
  • Up-to-date - Always fetches the latest documentation
  • Token efficient - No HTML boilerplate consuming context tokens

Webhook Transformers

Ping provides specialized webhook endpoints that automatically transform payloads from popular services into beautifully formatted notifications. Instead of setting up custom webhook handlers, point these services directly at Ping.

Railway

Receive Railway deployment notifications with auto-formatted messages.

Endpoint:

POST https://ping-api-production.up.railway.app/v1/webhook/railway/{token}

Setup:

  1. Go to your Railway project SettingsWebhooks
  2. Add a new webhook with your Ping URL
  3. Select the events you want to receive

Supported Events:

EventNotification
Deploy succeeded✅ Deploy Succeeded - service-name
Deploy failed❌ Deploy Failed - service-name
Deploy started🚀 Deploy Started - service-name
Deploy canceled⚠️ Deploy Canceled - service-name

Example Notification:

Title: ✅ Deploy Succeeded - api-server
Body: Deployed commit abc1234 to production
Action: [View Deploy] → Railway dashboard URL

GitHub

Receive GitHub Actions workflow and push event notifications.

Endpoint:

POST https://ping-api-production.up.railway.app/v1/webhook/github/{token}

Setup:

  1. Go to your GitHub repository SettingsWebhooks
  2. Add webhook with your Ping URL
  3. Content type: application/json
  4. Select events: Workflow runs and/or Push

Supported Events:

EventTriggerNotification
Workflow completedworkflow_run.completed✅/❌ Workflow name result
Pushpush📦 Push to branch - commit message

Example Workflow Notification:

Title: ✅ CI/CD Pipeline - success
Body: Workflow completed on main branch
Action: [View Run] → GitHub Actions URL

Example Push Notification:

Title: 📦 Push to main
Body: feat: add user authentication (abc1234)
Action: [View Commit] → GitHub commit URL

Vercel

Receive Vercel deployment notifications.

Endpoint:

POST https://ping-api-production.up.railway.app/v1/webhook/vercel/{token}

Setup:

  1. Go to your Vercel project SettingsWebhooks
  2. Add webhook with your Ping URL
  3. Select deployment events

Supported Events:

EventNotification
Deployment succeeded✅ Deploy Succeeded - project-name
Deployment failed❌ Deploy Failed - project-name
Deployment started🚀 Deploy Started - project-name
Deployment canceled⚠️ Deploy Canceled - project-name

Example Notification:

Title: ✅ Deploy Succeeded - my-website
Body: Deployed to production
Action: [View Deploy] → Vercel deployment URL

Transformer Behavior

All webhook transformers:

  • Auto-format payloads with emoji-prefixed titles
  • Extract relevant information (commit messages, service names, URLs)
  • Add actions with links to view deployments/runs
  • Handle all deployment states (success, failure, canceled, started)
  • Respect channel rate limits (same as regular webhooks)
  • Support channel muting

If the incoming payload can't be parsed, the transformer falls back to a generic notification with the raw data.


Bidirectional Workflows (Ping it Back)

The core value of Ping isn't just notifications - it's the ability to respond back. Send a ping, get a notification, ping back your response. This enables powerful human-in-the-loop workflows without running a server.

The Pattern

Send Ping  →  Notification  →  User Response  →  Continue Workflow
             (on phone)       (tap button)      (based on response)

Use Cases

Approval Workflows

Request approval before critical actions:

JSON
{
  "title": "Deploy to Production?",
  "body": "v2.1.0 - 47 files changed",
  "priority": "high",
  "actions": [
    {"type": "button", "label": "Deploy", "key": "deploy", "style": "primary"},
    {"type": "button", "label": "Cancel", "key": "cancel", "style": "destructive"}
  ]
}

Preview

Deploy to Production?

just now
v2.1.0 - 47 files changed

Examples:

  • CI/CD deployment approvals
  • Database migration confirmations
  • Access request approvals
  • Purchase order sign-offs
  • Content publication approvals

Quick Decisions

Get rapid input on simple choices:

JSON
{
  "title": "Server Alert: High CPU",
  "body": "web-server-01 at 95% CPU for 5 minutes",
  "actions": [
    {"type": "button", "label": "Scale Up", "key": "scale"},
    {"type": "button", "label": "Restart", "key": "restart"},
    {"type": "button", "label": "Ignore", "key": "ignore"}
  ]
}

Preview

Server Alert: High CPU

just now
web-server-01 at 95% CPU for 5 minutes

Examples:

  • Incident response decisions
  • Alert acknowledgments
  • Escalation triggers
  • A/B test selections

Data Collection

Gather structured input on the go:

JSON
{
  "title": "Quick Feedback",
  "body": "How was your experience?",
  "actions": [
    {"type": "select", "label": "Rating", "key": "rating", "options": [
      {"label": "Great", "value": "5"},
      {"label": "Good", "value": "4"},
      {"label": "Okay", "value": "3"},
      {"label": "Poor", "value": "2"}
    ]},
    {"type": "text", "label": "Comment", "key": "comment", "placeholder": "Optional feedback..."},
    {"type": "button", "label": "Submit", "key": "submit", "style": "primary"}
  ]
}

Preview

Quick Feedback

just now
How was your experience?

Examples:

  • Customer feedback collection
  • Bug report triage
  • Task prioritization
  • Quick surveys

AI Agent Guidance

Provide human oversight for AI systems:

JSON
{
  "title": "Agent Needs Input",
  "body": "Found 3 matching files. Which should I modify?",
  "actions": [
    {"type": "select", "label": "File", "key": "file", "options": [
      {"label": "auth.ts", "value": "src/auth.ts"},
      {"label": "login.ts", "value": "src/login.ts"},
      {"label": "session.ts", "value": "src/session.ts"}
    ]},
    {"type": "button", "label": "Proceed", "key": "proceed", "style": "primary"},
    {"type": "button", "label": "Cancel", "key": "cancel", "style": "destructive"}
  ]
}

Preview

Agent Needs Input

just now
Found 3 matching files. Which should I modify?

Examples:

  • Claude Code deploy approvals
  • Autonomous agent confirmations
  • LLM output validation
  • Multi-step workflow checkpoints

Implementation Patterns

Python (with requests)

import requests
import time

TOKEN = "YOUR_WEBHOOK_TOKEN"
API = "https://ping-api-production.up.railway.app"

def ping_and_wait(title, body, actions, timeout=300):
    """Send notification and wait for response."""

    # Send the notification
    response = requests.post(
        f"{API}/v1/send/{TOKEN}",
        json={"title": title, "body": body, "actions": actions}
    )
    msg_id = response.json()["id"]

    # Poll for response
    start = time.time()
    while time.time() - start < timeout:
        result = requests.get(f"{API}/v1/callback/{TOKEN}/{msg_id}").json()

        if result["status"] == "submitted":
            return result["result"]
        elif result["status"] in ("expired", "already_read", "failed"):
            return None

        time.sleep(3)

    return None  # Timeout

# Usage
result = ping_and_wait(
    title="Deploy to Production?",
    body="Version 2.1.0",
    actions=[
        {"type": "button", "label": "Deploy", "key": "deploy", "style": "primary"},
        {"type": "button", "label": "Cancel", "key": "cancel", "style": "destructive"}
    ]
)

if result and result["triggered_by"] == "deploy":
    print("Deploying...")
else:
    print("Cancelled or timeout")

Node.js (async/await)

const TOKEN = 'YOUR_WEBHOOK_TOKEN';
const API = 'https://ping-api-production.up.railway.app';

async function pingAndWait(title, body, actions, timeout = 300000) {
  // Send notification
  const sendRes = await fetch(`${API}/v1/send/${TOKEN}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, body, actions })
  });
  const { id: msgId } = await sendRes.json();

  // Poll for response
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const pollRes = await fetch(`${API}/v1/callback/${TOKEN}/${msgId}`);
    const result = await pollRes.json();

    if (result.status === 'submitted') return result.result;
    if (['expired', 'already_read', 'failed'].includes(result.status)) return null;

    await new Promise(r => setTimeout(r, 3000));
  }

  return null; // Timeout
}

// Usage
const result = await pingAndWait(
  'Approve Purchase?',
  'Order #1234 - $500',
  [
    { type: 'button', label: 'Approve', key: 'approve', style: 'primary' },
    { type: 'button', label: 'Reject', key: 'reject', style: 'destructive' }
  ]
);

if (result?.triggered_by === 'approve') {
  console.log('Processing order...');
}

Best Practices

  1. Set appropriate timeouts - 5 minutes works for most approval workflows
  2. Handle all response states - pending, submitted, expired, already_read, failed
  3. Use clear action labels - "Deploy to Production" is better than "Yes"
  4. Include context in the body - Version numbers, file counts, amounts
  5. Use priority wisely - Reserve "high" for truly urgent decisions
  6. Design for mobile - Keep messages concise, actions clear

Security Considerations

ProtectionHow It Works
One-time readResponses are cleared after retrieval
10-minute expiryStale responses auto-expire
Token authOnly your webhook token can poll
No overwriteCan't submit multiple responses

Web Dashboard

In addition to the iOS app, Ping offers a web dashboard for managing channels and viewing messages from your browser.

Accessing the Dashboard

  1. Visit getping.pro
  2. Click Sign in with Apple
  3. Complete authentication
  4. You'll be redirected to your dashboard

Managing Channels

The web dashboard provides the same channel management as the iOS app:

  • Create channels with custom names, icons, and colors
  • View webhook URLs and copy them
  • Edit channel settings
  • Delete channels
  • Regenerate tokens

Viewing Messages

The Inbox shows all messages across channels in chronological order. You can:

  • Filter by channel
  • View message details
  • See action buttons
  • Delete messages

Account

Sign In with Apple

Ping uses Sign in with Apple exclusively for authentication. This provides:

  • Security - No passwords to remember or steal
  • Privacy - Option to hide your email address
  • Convenience - Face ID/Touch ID authentication

Your Apple ID links your account across iOS and web.

Cross-Device Sync

Signing in with the same Apple ID on multiple devices syncs:

  • All your channels
  • Complete message history
  • Channel settings

Changes made on any device appear everywhere instantly.

Signing Out

On iOS

  1. Tap the Settings tab
  2. Tap Sign Out
  3. Confirm

On Web

  1. Click your account in the header
  2. Click Sign Out

Note: Signing out clears local data from the device but doesn't delete your account. Signing back in restores everything.

Deleting Your Account

You can permanently delete your account and all associated data. This action is irreversible.

On iOS

  1. Tap the Settings tab
  2. Scroll to Account
  3. Tap Delete Account
  4. Confirm deletion

On Web

  1. Go to Settings
  2. Click Delete Account
  3. Confirm deletion

Via API

Terminal
curl -X DELETE 'https://ping-api-production.up.railway.app/v1/auth/user' \
  -H 'Authorization: Bearer YOUR_SESSION_TOKEN'

Response:

JSON
{
  "ok": true,
  "message": "Account and all associated data deleted"
}

What Gets Deleted

Account deletion removes all your data in this order:

DataDescription
MessagesAll notification history
Channel pinsPin preferences
ChannelsAll channels and webhook URLs
FoldersChannel organization
DevicesRegistered push tokens
SessionsAll active sessions
SubscriptionsPlan and payment data
Usage recordsMonthly usage tracking
User accountYour Apple ID association

Warning: This action is permanent and cannot be undone. All webhook URLs will immediately stop working.


API Reference

Base URL

https://ping-api-production.up.railway.app

Authentication

Most endpoints support two authentication methods:

Session Token (iOS/Web)

Obtained via Sign in with Apple:

Authorization: Bearer <session_token>

API Key (Programmatic Access)

Create API keys in the app settings for server-to-server access:

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API keys work with all authenticated endpoints including channels, messages, and webhook secrets.

Webhook Endpoint

The webhook endpoint (POST /v1/send/:token) uses the URL token instead of a header - no Authorization header needed.

API Key Management

Create and manage API keys for programmatic access to the Ping API.

Key Format

API keys follow this format:

pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Prefix: pk_live_ (8 characters)
  • Random: 32 cryptographically secure characters
  • Total: 40 characters

Create an API Key

Terminal
curl -X POST 'https://ping-api-production.up.railway.app/v1/keys' \
  -H 'Authorization: Bearer YOUR_SESSION_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"name": "CI/CD Pipeline"}'

Response:

JSON
{
  "id": "key_xxxxxxxxxxxx",
  "name": "CI/CD Pipeline",
  "key": "pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "created_at": "2026-01-16T12:00:00Z"
}

Important: The full API key is only shown once at creation. Store it securely - you cannot retrieve it again.

List API Keys

Terminal
curl 'https://ping-api-production.up.railway.app/v1/keys' \
  -H 'Authorization: Bearer YOUR_SESSION_TOKEN'

Response:

JSON
{
  "keys": [
    {
      "id": "key_xxxxxxxxxxxx",
      "name": "CI/CD Pipeline",
      "prefix": "pk_live_xxxx",
      "created_at": "2026-01-16T12:00:00Z",
      "last_used_at": "2026-01-16T14:30:00Z"
    }
  ]
}

Note: Only the key prefix is shown - the full key is never returned after creation.

Revoke an API Key

Terminal
curl -X DELETE 'https://ping-api-production.up.railway.app/v1/keys/key_xxxxxxxxxxxx' \
  -H 'Authorization: Bearer YOUR_SESSION_TOKEN'

Response:

JSON
{
  "ok": true,
  "message": "API key revoked"
}

Limits and Security

LimitValue
Max active keys per user10
Key storageSHA-256 hashed
Usage trackinglast_used_at updated on use

Best Practices:

  • Give keys descriptive names (e.g., "GitHub Actions", "n8n Workflows")
  • Revoke keys you no longer use
  • Create separate keys for different services
  • Never commit keys to version control

Endpoints

Send Notification (Webhook)

POST /v1/send/{token}
Content-Type: application/json

Request Body:

JSON
{
  "title": "Required title",
  "body": "Optional body text",
  "priority": "normal",
  "actions": [
    {"type": "url", "label": "Button Text", "value": "https://example.com"}
  ]
}

Preview

Required title

just now
Optional body text

Response:

JSON
{"ok": true, "id": "msg_xxxxxxxxxxxx"}

Authentication

EndpointMethodDescription
/v1/auth/applePOSTSign in with Apple
/v1/auth/refreshPOSTRefresh session
/v1/auth/logoutPOSTSign out
/v1/auth/logout-allPOSTSign out all devices
/v1/auth/meGETGet current user
/v1/auth/userDELETEDelete account and all data

API Keys

EndpointMethodDescription
/v1/keysGETList all API keys
/v1/keysPOSTCreate new API key
/v1/keys/:idDELETERevoke API key

Devices

EndpointMethodDescription
/v1/devicesPOSTRegister device
/v1/devices/:idPATCHUpdate push token

Channels

EndpointMethodDescription
/v1/channelsGETList channels
/v1/channelsPOSTCreate channel
/v1/channels/:idGETGet channel
/v1/channels/:idPATCHUpdate channel
/v1/channels/:idDELETEDelete channel
/v1/channels/:id/regeneratePOSTRegenerate webhook token
/v1/channels/:id/secretGETGet webhook signature secret
/v1/channels/:id/secret/rotatePOSTRotate webhook signature secret

Signature Verification (Public)

EndpointMethodDescription
/v1/verify/docsGETSignature verification documentation
/v1/verify/generatePOSTGenerate a test signature
/v1/verify/signaturePOSTVerify a signature

Messages

EndpointMethodDescription
/v1/messagesGETGet messages
/v1/messages/:idDELETEDelete message
/v1/messages/:id/responseGETPoll for interactive action response

Webhook Transformers

EndpointMethodDescription
/v1/webhook/railway/:tokenPOSTRailway deployment webhook
/v1/webhook/github/:tokenPOSTGitHub Actions/push webhook
/v1/webhook/vercel/:tokenPOSTVercel deployment webhook

Troubleshooting

Not Receiving Notifications

1. Check Notification Permissions

On your iPhone:

  1. Open Settings
  2. Scroll to Ping
  3. Tap Notifications
  4. Ensure Allow Notifications is enabled

2. Verify Your Webhook URL

Test with curl and check the response:

Terminal
curl -X POST 'YOUR_WEBHOOK_URL' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Test"}'

Preview

Test

just now

You should see: {"ok": true, "id": "msg_..."}

3. Check Your Internet Connection

Both your sending system and iPhone need internet access.

4. Ensure You're Signed In

Open the app and verify you're signed in. If you see the sign-in screen, authenticate again.

5. Device-Specific Issues

  • Low Power Mode may delay notifications
  • Focus modes may filter notifications
  • Do Not Disturb silences notifications

Webhook Errors

invalid_json

Your request body isn't valid JSON.

Common causes:

  • Missing quotes around strings
  • Trailing commas
  • Special characters not escaped

Solution: Use a JSON validator or the printf method:

Terminal
printf '{"title":"Hello"}' | curl -s -X POST 'YOUR_URL' -H 'Content-Type: application/json' -d @-

invalid_payload

Missing required fields or exceeded limits.

Check:

  • title is present and under 100 characters
  • body (if present) is under 10,000 characters
  • priority is one of: low, normal, high

invalid_token

The channel doesn't exist or the URL is wrong.

Check:

  • The webhook URL is correct
  • The channel hasn't been deleted
  • You haven't regenerated the token

rate_limited

Too many requests to this channel.

Solution: Wait a moment and retry, or create multiple channels for high-volume use cases.

Authentication Issues

Can't Sign In

  1. Ensure you have a stable internet connection
  2. Check that Sign in with Apple works in other apps
  3. Try signing out of iCloud and back in

Session Expired

Sessions last 30 days. If you get authentication errors:

  1. Sign out
  2. Sign back in

Best Practices

Channel Organization

Create separate channels for different sources:

ChannelUse Case
Deploy AlertsCI/CD notifications
Server MonitoringInfrastructure alerts
Cron JobsScheduled task completion
ErrorsApplication errors
OrdersE-commerce notifications

Priority Usage

Use priority levels appropriately:

PriorityWhen to Use
highCritical alerts requiring immediate attention
normalRegular notifications (default)
lowInformational updates

Tip: Reserve high priority for truly urgent matters. Overuse reduces its effectiveness.

Security

Data protection:

  • All communication uses HTTPS/TLS encryption
  • Message content (title and body) is encrypted at rest using AES-256 authenticated encryption
  • Sign in with Apple, no passwords stored
  • Webhook tokens can be regenerated anytime

Keep your webhook URLs private:

  • Don't commit them to public repositories
  • Use environment variables or secrets managers
  • Regenerate tokens if exposed

Example with environment variables:

Terminal
# Set in your environment
export PING_URL="https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN"

# Use in scripts
curl -X POST "$PING_URL" -H 'Content-Type: application/json' -d '{"title": "Alert"}'

Preview

Alert

just now

Error Handling

Always check webhook responses in production:

const response = await fetch(WEBHOOK_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Alert' })
});

if (!response.ok) {
  const error = await response.json();
  console.error('Failed to send notification:', error);
  // Handle error (retry, fallback, etc.)
}

FAQ

Is Ping free?

Ping offers a free tier and a Pro plan:

FeatureFreePro
Channels3Unlimited
Messages/month1,000Unlimited
Message retention7 days90 days
Interactive actions
Webhook rate limit5/min200/min

The free tier is great for personal projects and testing. Upgrade to Pro for production workloads or interactive workflows.

How many channels can I create?

  • Free plan: Up to 1 channel
  • Pro plan: Unlimited channels

What's the message retention period?

  • Free plan: 7 days
  • Pro plan: 90 days

Messages are automatically deleted after the retention period. Consider upgrading to Pro for longer retention.

Can I use Ping for marketing notifications?

Ping is designed for developer notifications from automated systems. It's not intended for marketing or user-facing notifications.

Does Ping work on iPad?

Ping is currently iPhone-only. iPad support may come in a future update.

Can I use Ping on Android?

Ping is iOS-only as it relies on Apple Push Notifications. There are no current plans for Android support.

How fast are notifications delivered?

Typically within 1-2 seconds. Actual delivery time depends on network conditions and Apple's push notification service.

Can I send notifications to multiple devices?

Yes! Sign in with the same Apple ID on multiple devices, and all will receive notifications.

What happens if I delete a channel?

All messages in that channel are permanently deleted. The webhook URL stops working immediately.

Can I recover a deleted channel?

No. Channel deletion is permanent. You'll need to create a new channel and update your integrations.


Support


Last updated: January 2026