Callbacks & Polling
When using interactive actions, you can choose how to receive the user's response:
- Callback URL - The iOS app POSTs the response directly to your server
- Polling - 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"
}
}| 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) |
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 nowAlice requested access to production
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
Send the Notification
Terminal
# No callback_url needed - we'll poll for the response
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 nowDeploy v2.1.0 to production
Poll for the Response
Terminal
# Uses webhook token, no auth header 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": "..."}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
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"
}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 |
Testing Callbacks
Use the callback test server in the Ping repository to test callbacks locally:
Terminal
cd ping-api
# 1. Start the callback test server
node tests/callback-server.cjs 3001
# 2. In another terminal, start ngrok
ngrok http 3001
# 3. Copy the ngrok URL (e.g., https://abc123.ngrok-free.app)
# 4. Send a test notification with that callback URL
curl -X POST https://ping-api-production.up.railway.app/v1/send/YOUR_TOKEN \
-H "Content-Type: application/json" \
-d '{
"title": "Callback Test",
"callback_url": "https://abc123.ngrok-free.app",
"actions": [
{"type": "button", "label": "Approve", "key": "approve", "style": "primary"}
]
}'
# 5. Open the message in iOS app detail view and tap the button
# 6. Watch the callback server terminal for the incoming POSTPreview
Callback Test
just nowRelated
- Interactive Actions - Action types and options
- Webhook Signatures - Verify callback authenticity
- Ping it Back - Complete examples