Slack Pledge Approval Integration
Overview
Replace email-based pledge approval workflow with Slack interactive buttons. When someone submits a pledge, a Slack message appears with signer details and Approve/Reject buttons. Clicking a button updates WordPress directly.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Gravity Forms │ ──── │ WordPress │ ──── │ Slack │
│ (submission) │ │ (sends webhook)│ │ (notification)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
│ User clicks
│ Approve/Reject
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ pledge_signer │ ◀─── │ REST Endpoint │ ◀─── │ Slack Callback │
│ (status update)│ │ (receives POST)│ │ (button action)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
Components
1. Slack App Configuration
Create app at: https://api.slack.com/apps
App Settings: - App Name: California Forever Pledges (or similar) - Workspace: Client's Slack workspace
Features to enable:
| Feature | Purpose | Configuration |
|---|---|---|
| Incoming Webhooks | Send messages to Slack | Select target channel (e.g., #pledge-approvals) |
| Interactivity | Receive button clicks | Request URL: https://californiaforever.com/wp-json/cf/v1/slack/pledge-action |
Credentials needed (store in wp-config.php):
define( 'CF_SLACK_WEBHOOK_URL', 'https://hooks.slack.com/services/XXX/YYY/ZZZ' );
define( 'CF_SLACK_SIGNING_SECRET', 'your-signing-secret' );
2. WordPress → Slack (Send Notification)
Hook into existing Gravity Forms submission handler.
Slack Message Format (Block Kit):
┌────────────────────────────────────────────────┐
│ 🆕 New Pledge Submission │
├────────────────────────────────────────────────┤
│ │
│ *John Smith* │
│ Chief Innovation Officer │
│ Acme Corporation │
│ john@acme.com │
│ │
│ Submitted: Jan 8, 2026 at 2:34 PM │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Approve │ │ Reject │ │
│ └──────────┘ └──────────┘ │
│ │
└────────────────────────────────────────────────┘
PHP Implementation:
/**
* Send Slack notification when pledge is submitted
*
* @param int $post_id The pledge_signer post ID
* @param array $data Signer data (first_name, last_name, organization, position, email)
*/
function cf_send_pledge_to_slack( $post_id, $data ) {
$webhook_url = CF_SLACK_WEBHOOK_URL;
if ( empty( $webhook_url ) ) {
return;
}
$full_name = trim( $data['first_name'] . ' ' . $data['last_name'] );
$org_line = ! empty( $data['organization'] ) ? $data['organization'] : '';
$position_line = ! empty( $data['position'] ) ? $data['position'] : '';
// Build the message blocks
$blocks = array(
array(
'type' => 'header',
'text' => array(
'type' => 'plain_text',
'text' => '🆕 New Pledge Submission',
'emoji' => true,
),
),
array(
'type' => 'section',
'fields' => array(
array(
'type' => 'mrkdwn',
'text' => "*Name:*\n{$full_name}",
),
array(
'type' => 'mrkdwn',
'text' => "*Email:*\n{$data['email']}",
),
),
),
);
// Add org/position if present
if ( $org_line || $position_line ) {
$org_fields = array();
if ( $org_line ) {
$org_fields[] = array(
'type' => 'mrkdwn',
'text' => "*Organization:*\n{$org_line}",
);
}
if ( $position_line ) {
$org_fields[] = array(
'type' => 'mrkdwn',
'text' => "*Position:*\n{$position_line}",
);
}
$blocks[] = array(
'type' => 'section',
'fields' => $org_fields,
);
}
// Add timestamp
$blocks[] = array(
'type' => 'context',
'elements' => array(
array(
'type' => 'mrkdwn',
'text' => 'Submitted: ' . current_time( 'M j, Y \a\t g:i A' ),
),
),
);
// Add approve/reject buttons
$blocks[] = array(
'type' => 'actions',
'elements' => array(
array(
'type' => 'button',
'text' => array(
'type' => 'plain_text',
'text' => '✓ Approve',
'emoji' => true,
),
'style' => 'primary',
'action_id' => 'approve_pledge',
'value' => (string) $post_id,
),
array(
'type' => 'button',
'text' => array(
'type' => 'plain_text',
'text' => '✗ Reject',
'emoji' => true,
),
'style' => 'danger',
'action_id' => 'reject_pledge',
'value' => (string) $post_id,
),
),
);
$payload = array(
'blocks' => $blocks,
'text' => "New pledge from {$full_name}", // Fallback for notifications
);
wp_remote_post( $webhook_url, array(
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( $payload ),
'timeout' => 15,
) );
}
3. Slack → WordPress (REST Endpoint)
Endpoint: POST /wp-json/cf/v1/slack/pledge-action
What Slack sends: A POST with application/x-www-form-urlencoded body containing a payload parameter (JSON string).
PHP Implementation:
/**
* Register REST API endpoint for Slack interactions
*/
add_action( 'rest_api_init', function() {
register_rest_route( 'cf/v1', '/slack/pledge-action', array(
'methods' => 'POST',
'callback' => 'cf_handle_slack_pledge_action',
'permission_callback' => '__return_true', // Verification done in callback
) );
} );
/**
* Handle Slack button clicks
*/
function cf_handle_slack_pledge_action( WP_REST_Request $request ) {
// Get the raw body for signature verification
$body = $request->get_body();
$timestamp = $request->get_header( 'X-Slack-Request-Timestamp' );
$signature = $request->get_header( 'X-Slack-Signature' );
// Verify request is from Slack
if ( ! cf_verify_slack_signature( $body, $timestamp, $signature ) ) {
return new WP_REST_Response( array( 'error' => 'Invalid signature' ), 403 );
}
// Parse the payload
parse_str( $body, $parsed );
$payload = json_decode( $parsed['payload'], true );
if ( empty( $payload['actions'][0] ) ) {
return new WP_REST_Response( array( 'error' => 'No action found' ), 400 );
}
$action = $payload['actions'][0];
$action_id = $action['action_id']; // 'approve_pledge' or 'reject_pledge'
$post_id = intval( $action['value'] );
$user_name = $payload['user']['name'] ?? 'Someone';
// Verify the post exists and is a pledge_signer
$post = get_post( $post_id );
if ( ! $post || $post->post_type !== 'pledge_signer' ) {
return cf_slack_response_message( 'Error: Pledge not found.' );
}
// Get signer name for confirmation message
$signer_name = get_post_meta( $post_id, 'first_name', true ) . ' ' . get_post_meta( $post_id, 'last_name', true );
// Process the action
if ( $action_id === 'approve_pledge' ) {
update_post_meta( $post_id, 'approval_status', 'approved' );
$status_text = "✓ *Approved* by {$user_name}";
$color = '#22c55e'; // green
} else {
update_post_meta( $post_id, 'approval_status', 'rejected' );
$status_text = "✗ *Rejected* by {$user_name}";
$color = '#ef4444'; // red
}
// Return updated message (replaces original)
return cf_slack_updated_message( $payload, $signer_name, $status_text );
}
/**
* Verify Slack request signature
*/
function cf_verify_slack_signature( $body, $timestamp, $signature ) {
$signing_secret = CF_SLACK_SIGNING_SECRET;
// Check timestamp is within 5 minutes (prevent replay attacks)
if ( abs( time() - intval( $timestamp ) ) > 300 ) {
return false;
}
// Compute expected signature
$sig_basestring = 'v0:' . $timestamp . ':' . $body;
$expected_signature = 'v0=' . hash_hmac( 'sha256', $sig_basestring, $signing_secret );
return hash_equals( $expected_signature, $signature );
}
/**
* Build updated Slack message after action
*/
function cf_slack_updated_message( $original_payload, $signer_name, $status_text ) {
// Get original blocks and remove the action buttons
$blocks = $original_payload['message']['blocks'] ?? array();
// Remove the actions block (buttons)
$blocks = array_filter( $blocks, function( $block ) {
return $block['type'] !== 'actions';
} );
// Add status block
$blocks[] = array(
'type' => 'section',
'text' => array(
'type' => 'mrkdwn',
'text' => $status_text,
),
);
// Return the response that updates the original message
return new WP_REST_Response( array(
'replace_original' => true,
'blocks' => array_values( $blocks ),
), 200 );
}
4. Integration Points
Modify existing Gravity Forms handler in inc/post-types/pledge-signers.php:
// After creating the pledge_signer post...
add_action( 'gform_after_submission_X', function( $entry, $form ) {
// ... existing CPT creation code ...
// Add Slack notification
if ( $post_id ) {
cf_send_pledge_to_slack( $post_id, array(
'first_name' => rgar( $entry, '1' ), // Adjust field IDs
'last_name' => rgar( $entry, '2' ),
'organization' => rgar( $entry, '3' ),
'position' => rgar( $entry, '4' ),
'email' => rgar( $entry, '5' ),
) );
}
}, 10, 2 );
File Structure
inc/
├── post-types/
│ └── pledge-signers.php # Existing - add Slack call after CPT creation
└── integrations/
└── slack-pledge-approval.php # NEW - all Slack-related code
Load in functions.php:
require_once get_template_directory() . '/inc/integrations/slack-pledge-approval.php';
Setup Checklist
Slack App Setup (Client's workspace)
- [ ] Go to https://api.slack.com/apps → Create New App
- [ ] Choose "From scratch", name it, select workspace
- [ ] Enable Incoming Webhooks → Add to channel (e.g., #pledge-approvals)
- [ ] Copy webhook URL
- [ ] Enable Interactivity & Shortcuts
- [ ] Set Request URL:
https://californiaforever.com/wp-json/cf/v1/slack/pledge-action - [ ] Copy Signing Secret from Basic Information
WordPress Setup
- [ ] Add constants to
wp-config.php(or environment variables) - [ ] Create
inc/integrations/slack-pledge-approval.php - [ ] Update Gravity Forms hook to call Slack function
- [ ] Deploy to production
- [ ] Test with a real submission
Security Considerations
- Request Verification: Every incoming request from Slack is verified using HMAC signature
- Timestamp Check: Requests older than 5 minutes are rejected (prevents replay attacks)
- Post Type Validation: Only
pledge_signerposts can be modified - No sensitive data in Slack: Email shown but no other PII exposed unnecessarily
Testing Notes
- Sending to Slack: Works from local (outbound HTTP)
- Button callbacks: Only work on production (Slack needs public URL)
- Local testing option: Use ngrok to tunnel local site for testing callbacks
Optional Enhancements
- View in Admin button: Add a third button linking to the WP admin edit screen
- Duplicate detection: Check if email already exists, warn in Slack message
- Daily digest: Instead of per-submission, batch notifications
- Undo window: Allow reversing approval within X minutes
Questions for Client
- Which Slack channel? Should this go to an existing channel or a new dedicated one?
- Who creates the Slack app? Do they want to create it, or should we do it with temporary admin access?
- Additional notifications? Should Slack also notify when CSV imports happen (bulk approvals)?