Ping

Callbacks & Polling

When using interactive actions, you can choose how to receive the user's response:

  1. Callback URL - The iOS app POSTs the response directly to your server
  2. 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"
  }
}
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)

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

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 now
Deploy 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

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"
}

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

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 POST

Preview

Callback Test

just now