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)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 │ │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 nowv2.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 nowweb-server-01 at 95% CPU for 5 minutes
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 nowHow was your experience?
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 nowFound 3 matching files. Which should I modify?
Complete Example
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 1Preview
Deploy to Production?
just nowVersion 2.1.0 ready for release
Implementation Patterns
Python
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
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...');
}Security Protections
| Protection | Description |
|---|---|
| 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 |
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 |
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