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)

WordPress Setup


Security Considerations

  1. Request Verification: Every incoming request from Slack is verified using HMAC signature
  2. Timestamp Check: Requests older than 5 minutes are rejected (prevents replay attacks)
  3. Post Type Validation: Only pledge_signer posts can be modified
  4. No sensitive data in Slack: Email shown but no other PII exposed unnecessarily

Testing Notes


Optional Enhancements

  1. View in Admin button: Add a third button linking to the WP admin edit screen
  2. Duplicate detection: Check if email already exists, warn in Slack message
  3. Daily digest: Instead of per-submission, batch notifications
  4. Undo window: Allow reversing approval within X minutes

Questions for Client

  1. Which Slack channel? Should this go to an existing channel or a new dedicated one?
  2. Who creates the Slack app? Do they want to create it, or should we do it with temporary admin access?
  3. Additional notifications? Should Slack also notify when CSV imports happen (bulk approvals)?