Ping Documentation
Webhook-to-push notifications for developers. POST JSON to a URL, get a notification on your phone.
Table of Contents
- Introduction
- Getting Started
- Channels
- Webhooks
- Integrations
- Webhook Transformers
- Bidirectional Workflows (Ping it Back)
- Web Dashboard
- Account
- API Reference
- Troubleshooting
- Best Practices
- FAQ
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 POSTs JSON to your webhook URL
- Ping stores the message and sends it via Apple Push Notifications
- You receive an instant notification on your iPhone
Getting Started
Installation
- Download Ping from TestFlight (App Store release coming soon)
- 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:
- Open Settings on your iPhone
- Scroll to Ping
- Tap Notifications
- 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.
- Tap the Channels tab
- Tap the + button
- Enter a name (e.g., "Server Alerts")
- Choose an icon and color
- Tap Create
Send Your First Notification
Copy your webhook URL and send a test notification:
curl -X POST 'YOUR_WEBHOOK_URL' \
-H 'Content-Type: application/json' \
-d '{"title": "Hello from Ping!"}'Preview
Hello from Ping!
just nowYou 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
- Open the Channels tab
- Tap the + button
- Fill in the details:
- Name: Something descriptive
- Icon: Choose from available SF Symbols
- Color: Pick a color for visual identification
- Tap Create
Your new channel appears with a unique webhook URL.
Suggested Channels:
Deploy Alerts- CI/CD notificationsServer Monitoring- Infrastructure alertsn8n Workflows- Automation updatesCron Jobs- Scheduled task completionError Logs- Application errors
Managing Channels
Viewing Channel Details
Tap any channel to see:
- Message history (newest first)
- Webhook URL
- Channel settings
Editing a Channel
- Tap the channel
- Tap the settings icon
- Modify name, icon, or color
- Tap Save
Deleting a Channel
- Swipe left on the channel in the list
- 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
- Tap the channel
- Tap Settings
- Tap Mute Channel
- Select a duration (1 hour, 8 hours, 24 hours, or custom)
How Muting Works
| Behavior | Muted | Active |
|---|---|---|
| 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:
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:
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
- Tap the channel
- Tap the webhook URL to copy it
- Paste it in your integration
Regenerating Tokens
If your webhook URL is compromised:
- Tap the channel
- Tap Settings
- Tap Regenerate Token
- 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:
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 nowPayload Reference
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
title | string | Yes | Notification title | Max 100 characters |
body | string | No | Notification body (supports Markdown) | Max 10,000 characters |
priority | string | No | Delivery priority | low, normal, high |
callback_url | string | Conditional | URL to receive action responses | Required for interactive actions |
actions | array | No | Interactive actions | Max 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
{
"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 nowThis 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
{
"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 nowInteractive Example with Callback
{
"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 nowPriority Levels
| Priority | Behavior |
|---|---|
high | Plays a sound, shows alert indicator |
normal | Standard notification (default) |
low | Lower 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:
- URL actions - Open links in Safari (no callback needed)
- Interactive actions - Collect user input and POST responses to your callback URL
Action Types
| Type | Description | Requires key | Requires callback_url |
|---|---|---|---|
url | Opens external URL | No | No |
button | Clickable button | Optional | Yes |
select | Dropdown menu | Yes | Yes |
text | Text input field | Yes | Yes |
number | Numeric input | Yes | Yes |
toggle | On/off switch | Yes | Yes |
URL Actions
Open links directly in Safari. No callback URL required.
{
"type": "url",
"label": "View Build",
"value": "https://github.com/org/repo/actions/runs/123"
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "url" | |
label | string | Yes | Button text | Max 30 characters |
value | string | Yes | URL to open | Max 500 characters, valid URL |
Button Actions
Clickable buttons that trigger callbacks. Ideal for approve/reject workflows.
{
"type": "button",
"label": "Approve",
"key": "approve",
"style": "primary"
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "button" | |
label | string | Yes | Button text | Max 30 characters |
key | string | No | Identifier for callback | Max 50 characters |
style | string | No | Visual style | primary, secondary, destructive |
Select Actions
Dropdown menu for selecting from predefined options.
{
"type": "select",
"label": "Priority",
"key": "priority",
"options": [
{"label": "Low", "value": "low"},
{"label": "Medium", "value": "medium"},
{"label": "High", "value": "high"}
],
"value": "medium"
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "select" | |
label | string | Yes | Label text | Max 30 characters |
key | string | Yes | Identifier for callback | Max 50 characters |
options | array | Yes | Selection options | Max 10 options |
options[].label | string | Yes | Display text | Max 50 characters |
options[].value | string | Yes | Value sent in callback | Max 100 characters |
value | string | No | Default selection |
Text Input Actions
Free-form text entry field.
{
"type": "text",
"label": "Reply",
"key": "message",
"placeholder": "Type your message...",
"multiline": true
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "text" | |
label | string | Yes | Label text | Max 30 characters |
key | string | Yes | Identifier for callback | Max 50 characters |
placeholder | string | No | Placeholder text | Max 100 characters |
multiline | boolean | No | Allow multiple lines | Default: false |
value | string | No | Default text |
Number Input Actions
Numeric input with optional min/max bounds.
{
"type": "number",
"label": "Quantity",
"key": "quantity",
"min": 1,
"max": 100,
"defaultValue": 10
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "number" | |
label | string | Yes | Label text | Max 30 characters |
key | string | Yes | Identifier for callback | Max 50 characters |
min | number | No | Minimum value | |
max | number | No | Maximum value | |
defaultValue | number | No | Default value |
Toggle Actions
Boolean on/off switch.
{
"type": "toggle",
"label": "Enable notifications",
"key": "enabled",
"value": "false"
}| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
type | string | Yes | Must be "toggle" | |
label | string | Yes | Label text | Max 30 characters |
key | string | Yes | Identifier for callback | Max 50 characters |
value | string | No | Default 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:
- Callback URL - Provide a
callback_urland the iOS app POSTs the response directly to your server - Polling - Skip the
callback_urland 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:
{
"message_id": "msg_xxxxxxxxxxxx",
"channel_id": "ch_xxxxxxxxxxxx",
"timestamp": "2026-01-12T20:18:13Z",
"triggered_by": "approve",
"responses": {
"rating": "good",
"comment": "User entered text"
}
}| Field | Description |
|---|---|
message_id | The message ID that was interacted with |
channel_id | The channel the message belongs to |
timestamp | ISO8601 timestamp of when the action was triggered |
triggered_by | The key of the button/action that was tapped |
responses | Object 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
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 nowExample: Complex Form
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 nowOption 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
# 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# 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:
| Protection | Description |
|---|---|
| Single read | Responses can only be read once, then they're cleared |
| Auto-expire | Responses expire after 10 minutes if not retrieved |
| Token auth | Uses your webhook token (no session needed) |
| No overwrite | Cannot submit multiple responses to the same message |
Polling Response Statuses:
| Status | Meaning |
|---|---|
pending | User hasn't responded yet |
submitted | User responded - check result field |
expired | Response expired (10-minute TTL) |
already_read | Response was already consumed |
failed | Callback delivery failed |
Full Polling Response:
{
"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"
}| Field | Description |
|---|---|
triggered_by | The key of the button that was tapped |
responses | Form field values (keyed by field key) |
timestamp | When the user submitted their response |
Complete Polling Script:
#!/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 1Preview
Deploy to Production?
just nowConstraints
- 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_urlis optional - use it for server-to-server callbacks, or skip it and poll for responsescallback_urlis 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 generatedv1- HMAC-SHA256 signature of"timestamp.payload"
Getting Your Webhook Secret
Each channel has a unique secret for signature verification:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID/secret"Response:
{
"webhook_secret": "4140db9a5fa04fe3208183ceb1d659cafbfa23ff3f31e7cc3777fd3062e60992",
"algorithm": "HMAC-SHA256",
"header_name": "X-Ping-Signature"
}Verification Steps
- Extract
t(timestamp) andv1(signature) from the header - Check timestamp is within 5 minutes (replay protection)
- Reconstruct:
"${timestamp}.${rawBody}" - Compute HMAC-SHA256 with your webhook secret
- 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:
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:
# 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
{
"ok": true,
"id": "msg_xxxxxxxxxxxx",
"muted": false
}| Field | Description |
|---|---|
ok | Always true on success |
id | Unique message identifier for tracking or polling |
muted | true 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
{
"error": "error_code",
"message": "Human-readable description"
}Error Handling
| Error Code | HTTP Status | Description | Solution |
|---|---|---|---|
invalid_json | 400 | Request body isn't valid JSON | Check JSON syntax |
invalid_payload | 400 | Missing title, exceeds limits, or missing callback_url for interactive actions | Verify required fields and add callback_url |
invalid_token | 401 | Channel not found | Check webhook URL |
rate_limited | 429 | Too many requests | Wait and retry |
Common invalid_payload errors:
- Missing
titlefield callback_urlmissing when using interactive actions (button, select, text, number, toggle)callback_urlis not a valid URL- Field exceeds character limits
Rate Limits
Rate limits vary by plan:
| Plan | Webhook Rate | Other Endpoints |
|---|---|---|
| Free | 5/min per channel | 60/min per endpoint |
| Pro | 200/min per channel | 200/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:
{
"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
curl -X POST 'YOUR_WEBHOOK_URL' \
-H 'Content-Type: application/json' \
-d '{"title": "Task Complete"}'Preview
Task Complete
just nowWith Body
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 nowHigh Priority Alert
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 nowHandling Special Characters
If your message contains special characters, use printf:
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 NoneGitHub 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 nowSetup
- Go to your repository Settings > Secrets and variables > Actions
- Click New repository secret
- Name:
PING_WEBHOOK_URL - Value: Your webhook URL
- Click Add secret
n8n
Use HTTP Request nodes to send notifications and receive responses from your n8n workflows.
Simple Notification
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN |
| Body Content Type | JSON |
{
"title": "Workflow Complete",
"body": "{{ $json.summary }}",
"priority": "normal"
}Preview
Workflow Complete
just nowBidirectional 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)
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN |
{
"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 nowStep 2: Loop Node (Poll for Response)
Add a Loop node that polls until a response is received:
| Setting | Value |
|---|---|
| Loop Type | While |
| Condition | {{ $json.status === 'pending' }} |
| Max Iterations | 100 |
| Delay | 3000ms |
Inside the loop, add an HTTP Request:
| Setting | Value |
|---|---|
| Method | GET |
| URL | https://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:
{
"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 nowShell Scripts
Create a reusable notification function:
#!/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"
fiDocker
Send notifications from Docker containers:
# 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 nowIn 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 nowCron Jobs
Notify when scheduled tasks complete:
# 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 nowClaude 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
- Create a channel for Claude Code notifications in the Ping app
- Copy your webhook token
- Add to your project's
.claude/settings.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
}
]
}
]
}
}- Copy the hook scripts to your
.claude/hooks/directory (available in the Ping repository)
Deploy Approval Flow
When enabled, the deploy approval hook:
- Detects deploy commands (
git push,railway up,vercel deploy, etc.) - Sends a notification to your phone with Approve/Reject buttons
- Waits up to 5 minutes for your response
- If approved: runs the command
- 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:
curl -H 'Accept: text/markdown' https://getping.pro/docsThis returns the raw markdown content instead of the HTML page.
Claude Code
Add Ping documentation to your Claude Code context:
# Read the docs directly into context
curl -sH 'Accept: text/markdown' https://getping.pro/docs | claude --readOr 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/markdownCursor
Add as a documentation source in Cursor settings:
- Open Cursor Settings > Features > Docs
- Click Add new doc
- Enter URL:
https://getping.pro/docs - 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:
{
"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:
- Go to your Railway project Settings → Webhooks
- Add a new webhook with your Ping URL
- Select the events you want to receive
Supported Events:
| Event | Notification |
|---|---|
| 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 URLGitHub
Receive GitHub Actions workflow and push event notifications.
Endpoint:
POST https://ping-api-production.up.railway.app/v1/webhook/github/{token}
Setup:
- Go to your GitHub repository Settings → Webhooks
- Add webhook with your Ping URL
- Content type:
application/json - Select events: Workflow runs and/or Push
Supported Events:
| Event | Trigger | Notification |
|---|---|---|
| Workflow completed | workflow_run.completed | ✅/❌ Workflow name result |
| Push | push | 📦 Push to branch - commit message |
Example Workflow Notification:
Title: ✅ CI/CD Pipeline - success
Body: Workflow completed on main branch
Action: [View Run] → GitHub Actions URLExample Push Notification:
Title: 📦 Push to main
Body: feat: add user authentication (abc1234)
Action: [View Commit] → GitHub commit URLVercel
Receive Vercel deployment notifications.
Endpoint:
POST https://ping-api-production.up.railway.app/v1/webhook/vercel/{token}
Setup:
- Go to your Vercel project Settings → Webhooks
- Add webhook with your Ping URL
- Select deployment events
Supported Events:
| Event | Notification |
|---|---|
| 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 URLTransformer 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:
{
"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 nowExamples:
- 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:
{
"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 nowExamples:
- Incident response decisions
- Alert acknowledgments
- Escalation triggers
- A/B test selections
Data Collection
Gather structured input on the go:
{
"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 nowExamples:
- Customer feedback collection
- Bug report triage
- Task prioritization
- Quick surveys
AI Agent Guidance
Provide human oversight for AI systems:
{
"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 nowExamples:
- 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
- Set appropriate timeouts - 5 minutes works for most approval workflows
- Handle all response states - pending, submitted, expired, already_read, failed
- Use clear action labels - "Deploy to Production" is better than "Yes"
- Include context in the body - Version numbers, file counts, amounts
- Use priority wisely - Reserve "high" for truly urgent decisions
- Design for mobile - Keep messages concise, actions clear
Security Considerations
| Protection | How It Works |
|---|---|
| One-time read | Responses are cleared after retrieval |
| 10-minute expiry | Stale responses auto-expire |
| Token auth | Only your webhook token can poll |
| No overwrite | Can'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
- Visit getping.pro
- Click Sign in with Apple
- Complete authentication
- 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
- Tap the Settings tab
- Tap Sign Out
- Confirm
On Web
- Click your account in the header
- 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
- Tap the Settings tab
- Scroll to Account
- Tap Delete Account
- Confirm deletion
On Web
- Go to Settings
- Click Delete Account
- Confirm deletion
Via API
curl -X DELETE 'https://ping-api-production.up.railway.app/v1/auth/user' \
-H 'Authorization: Bearer YOUR_SESSION_TOKEN'Response:
{
"ok": true,
"message": "Account and all associated data deleted"
}What Gets Deleted
Account deletion removes all your data in this order:
| Data | Description |
|---|---|
| Messages | All notification history |
| Channel pins | Pin preferences |
| Channels | All channels and webhook URLs |
| Folders | Channel organization |
| Devices | Registered push tokens |
| Sessions | All active sessions |
| Subscriptions | Plan and payment data |
| Usage records | Monthly usage tracking |
| User account | Your 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
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:
{
"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
curl 'https://ping-api-production.up.railway.app/v1/keys' \
-H 'Authorization: Bearer YOUR_SESSION_TOKEN'Response:
{
"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
curl -X DELETE 'https://ping-api-production.up.railway.app/v1/keys/key_xxxxxxxxxxxx' \
-H 'Authorization: Bearer YOUR_SESSION_TOKEN'Response:
{
"ok": true,
"message": "API key revoked"
}Limits and Security
| Limit | Value |
|---|---|
| Max active keys per user | 10 |
| Key storage | SHA-256 hashed |
| Usage tracking | last_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/jsonRequest Body:
{
"title": "Required title",
"body": "Optional body text",
"priority": "normal",
"actions": [
{"type": "url", "label": "Button Text", "value": "https://example.com"}
]
}Preview
Required title
just nowResponse:
{"ok": true, "id": "msg_xxxxxxxxxxxx"}Authentication
| Endpoint | Method | Description |
|---|---|---|
/v1/auth/apple | POST | Sign in with Apple |
/v1/auth/refresh | POST | Refresh session |
/v1/auth/logout | POST | Sign out |
/v1/auth/logout-all | POST | Sign out all devices |
/v1/auth/me | GET | Get current user |
/v1/auth/user | DELETE | Delete account and all data |
API Keys
| Endpoint | Method | Description |
|---|---|---|
/v1/keys | GET | List all API keys |
/v1/keys | POST | Create new API key |
/v1/keys/:id | DELETE | Revoke API key |
Devices
| Endpoint | Method | Description |
|---|---|---|
/v1/devices | POST | Register device |
/v1/devices/:id | PATCH | Update push token |
Channels
| Endpoint | Method | Description |
|---|---|---|
/v1/channels | GET | List channels |
/v1/channels | POST | Create channel |
/v1/channels/:id | GET | Get channel |
/v1/channels/:id | PATCH | Update channel |
/v1/channels/:id | DELETE | Delete channel |
/v1/channels/:id/regenerate | POST | Regenerate webhook token |
/v1/channels/:id/secret | GET | Get webhook signature secret |
/v1/channels/:id/secret/rotate | POST | Rotate webhook signature secret |
Signature Verification (Public)
| Endpoint | Method | Description |
|---|---|---|
/v1/verify/docs | GET | Signature verification documentation |
/v1/verify/generate | POST | Generate a test signature |
/v1/verify/signature | POST | Verify a signature |
Messages
| Endpoint | Method | Description |
|---|---|---|
/v1/messages | GET | Get messages |
/v1/messages/:id | DELETE | Delete message |
/v1/messages/:id/response | GET | Poll for interactive action response |
Webhook Transformers
| Endpoint | Method | Description |
|---|---|---|
/v1/webhook/railway/:token | POST | Railway deployment webhook |
/v1/webhook/github/:token | POST | GitHub Actions/push webhook |
/v1/webhook/vercel/:token | POST | Vercel deployment webhook |
Troubleshooting
Not Receiving Notifications
1. Check Notification Permissions
On your iPhone:
- Open Settings
- Scroll to Ping
- Tap Notifications
- Ensure Allow Notifications is enabled
2. Verify Your Webhook URL
Test with curl and check the response:
curl -X POST 'YOUR_WEBHOOK_URL' \
-H 'Content-Type: application/json' \
-d '{"title": "Test"}'Preview
Test
just nowYou 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:
printf '{"title":"Hello"}' | curl -s -X POST 'YOUR_URL' -H 'Content-Type: application/json' -d @-invalid_payload
Missing required fields or exceeded limits.
Check:
titleis present and under 100 charactersbody(if present) is under 10,000 characterspriorityis 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
- Ensure you have a stable internet connection
- Check that Sign in with Apple works in other apps
- Try signing out of iCloud and back in
Session Expired
Sessions last 30 days. If you get authentication errors:
- Sign out
- Sign back in
Best Practices
Channel Organization
Create separate channels for different sources:
| Channel | Use Case |
|---|---|
Deploy Alerts | CI/CD notifications |
Server Monitoring | Infrastructure alerts |
Cron Jobs | Scheduled task completion |
Errors | Application errors |
Orders | E-commerce notifications |
Priority Usage
Use priority levels appropriately:
| Priority | When to Use |
|---|---|
high | Critical alerts requiring immediate attention |
normal | Regular notifications (default) |
low | Informational 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:
# 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 nowError 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:
| Feature | Free | Pro |
|---|---|---|
| Channels | 3 | Unlimited |
| Messages/month | 1,000 | Unlimited |
| Message retention | 7 days | 90 days |
| Interactive actions | ❌ | ✅ |
| Webhook rate limit | 5/min | 200/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
- GitHub Issues: github.com/actanonverbos/ping-api/issues
- Web Dashboard: getping.pro
Last updated: January 2026