ation**: Pass explicit arguments to wp_remote_post(), including timeout limits, content-type headers, and authentication tokens. Never rely on defaults for external integrations.
5. Response Handling: Treat the return value as a WP_Error or HTTP response array. Extract status codes, headers, and body content using dedicated WP functions. Log outcomes for audit trails.
Implementation
<?php
/**
* Handles Contact Form 7 submissions and dispatches payloads to external APIs.
* Attached to wpcf7_submit to guarantee execution regardless of mail settings.
*/
class FormPayloadDispatcher {
private const TARGET_ENDPOINT = 'https://api.example.com/v1/leads';
private const AUTH_TOKEN = 'sk_live_a1b2c3d4e5f6';
private const REQUEST_TIMEOUT = 12;
public function __construct() {
add_action( 'wpcf7_submit', [ $this, 'handle_submission' ], 10, 2 );
}
public function handle_submission( $contact_form, $result ) {
$form_id = $contact_form->id();
// Scope integration to specific form IDs
if ( ! in_array( $form_id, [ 42, 88 ], true ) ) {
return;
}
$submission = WPCF7_Submission::get_instance();
if ( ! $submission ) {
return;
}
$raw_inputs = $submission->get_posted_data();
$payload = $this->build_payload( $raw_inputs );
if ( empty( $payload ) ) {
error_log( 'CF7 Dispatcher: Empty payload generated. Aborting request.' );
return;
}
$this->dispatch_to_api( $payload );
}
private function build_payload( array $raw_inputs ): array {
$sanitized = [];
$sanitized['full_name'] = sanitize_text_field( $raw_inputs['user_fullname'] ?? '' );
$sanitized['contact_number'] = sanitize_text_field( $raw_inputs['user_phone'] ?? '' );
$sanitized['campaign_tag'] = 'promo_2024';
$sanitized['source'] = 'wordpress_cf7';
// Remove empty values to keep payload lean
return array_filter( $sanitized, static function( $value ) {
return $value !== '';
} );
}
private function dispatch_to_api( array $payload ): void {
$request_args = [
'body' => wp_json_encode( $payload ),
'headers' => [
'Authorization' => 'Bearer ' . self::AUTH_TOKEN,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'timeout' => self::REQUEST_TIMEOUT,
'sslverify' => true,
];
$response = wp_remote_post( self::TARGET_ENDPOINT, $request_args );
$this->evaluate_response( $response, $payload );
}
private function evaluate_response( $response, array $payload ): void {
if ( is_wp_error( $response ) ) {
error_log( sprintf(
'CF7 Dispatcher Network Error: %s | Payload: %s',
$response->get_error_message(),
wp_json_encode( $payload )
) );
return;
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( $status_code >= 200 && $status_code < 300 ) {
error_log( 'CF7 Dispatcher Success: ' . $body );
} else {
error_log( sprintf(
'CF7 Dispatcher HTTP %d: %s | Payload: %s',
$status_code,
$body,
wp_json_encode( $payload )
) );
}
}
}
// Initialize dispatcher
new FormPayloadDispatcher();
Why This Structure Works
- Class-based encapsulation prevents global namespace pollution and allows future extension (e.g., adding retry logic or webhook signatures).
wp_json_encode() replaces json_encode() to ensure WordPress handles character encoding and escaping consistently.
- Explicit timeout and SSL verification prevent hanging requests and man-in-the-middle vulnerabilities.
- Response evaluation separates network failures (
WP_Error) from HTTP status failures, enabling targeted alerting.
- Payload filtering removes empty fields before serialization, reducing bandwidth and preventing downstream validation errors.
Pitfall Guide
1. Hook Lifecycle Mismatch
Explanation: Attaching outbound requests to wpcf7_before_send_mail ties execution to the email pipeline. If the form lacks mail configuration or email sending is disabled, the hook never fires.
Fix: Use wpcf7_submit for API integrations. Reserve mail hooks for email-specific transformations.
2. String Interpolation Traps
Explanation: Single-quoted strings in PHP do not evaluate variables. 'Bearer $token' sends the literal characters $token instead of the credential value, causing 401 Unauthorized responses.
Fix: Use concatenation ('Bearer ' . $token) or double quotes ("Bearer $token"). Prefer concatenation for clarity and performance.
3. Array vs Scalar Type Confusion
Explanation: Square brackets [] in PHP create arrays. Assigning $token = [12345] produces an array, not a string or integer. When passed to json_encode() or header construction, it serializes as [12345] or triggers type warnings.
Fix: Use scalar assignment: $token = '12345';. Validate types with is_string() or ctype_alnum() before transmission.
4. JSON Serialization Gaps
Explanation: Unclosed parentheses in json_encode() calls cause parse errors. Missing commas between array keys break PHP array syntax. These errors halt execution before the HTTP request initiates.
Fix: Always close serialization functions. Use modern array syntax with explicit keys. Run code through php -l or a linter before deployment.
5. Unsanitized Payload Injection
Explanation: Passing raw $_POST or CF7 submitted data directly to external APIs exposes the integration to XSS, SQLi, or header injection if the target system echoes or stores values unsafely.
Fix: Apply sanitize_text_field(), sanitize_email(), or intval() based on expected data types. Strip HTML entities and control characters before serialization.
6. Ignoring WP HTTP Response Objects
Explanation: Treating wp_remote_post() output as a raw string or boolean discards critical diagnostic data. The function returns a WP_Error object on failure or an associative array containing headers, body, response, and cookies.
Fix: Always check is_wp_error() first. Extract status codes with wp_remote_retrieve_response_code() and bodies with wp_remote_retrieve_body().
7. SSL Verification Bypass in Production
Explanation: Setting 'sslverify' => false during debugging resolves certificate chain issues but leaves the integration vulnerable to MITM attacks. Leaving it disabled in production violates security baselines.
Fix: Use 'sslverify' => true in production. If certificates fail, update the server's CA bundle or configure WP_HTTP_BLOCK_EXTERNAL exceptions properly. Never disable verification permanently.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single form, low volume (<50/day) | Synchronous wp_remote_post on wpcf7_submit | Simple implementation, immediate feedback | Low (developer time) |
| Multi-form, high volume (>200/day) | Async queue with WP Cron or external worker | Prevents request timeout, decouples form submission from network latency | Medium (infrastructure + queue management) |
| Client delivery, frequent API changes | UI-driven plugin or configuration file | Non-technical users can update endpoints/tokens without code deployment | Low-Medium (plugin licensing or config overhead) |
| Strict compliance (GDPR/HIPAA) | Payload encryption + audit logging + data retention policy | Ensures data protection, meets regulatory requirements | High (encryption layer + compliance auditing) |
Configuration Template
<?php
/**
* Production-ready CF7 API Dispatcher Configuration
* Place in mu-plugins or theme functions.php
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'CF7_API_DISPATCHER_ENABLED', true );
define( 'CF7_API_TARGET_URL', 'https://api.yourdomain.com/v1/integrations' );
define( 'CF7_API_AUTH_TOKEN', 'your_secure_token_here' );
define( 'CF7_API_TIMEOUT', 10 );
define( 'CF7_API_LOG_LEVEL', 'error' ); // 'error' or 'debug'
add_action( 'wpcf7_submit', function( $contact_form, $result ) {
if ( ! CF7_API_DISPATCHER_ENABLED ) {
return;
}
$form_id = $contact_form->id();
$allowed_forms = [ 101, 205 ];
if ( ! in_array( $form_id, $allowed_forms, true ) ) {
return;
}
$submission = WPCF7_Submission::get_instance();
if ( ! $submission ) {
return;
}
$posted = $submission->get_posted_data();
$payload = [
'event' => 'form_submission',
'form_id' => $form_id,
'data' => [
'name' => sanitize_text_field( $posted['your-name'] ?? '' ),
'email' => sanitize_email( $posted['your-email'] ?? '' ),
'phone' => sanitize_text_field( $posted['your-phone'] ?? '' ),
],
'meta' => [
'timestamp' => current_time( 'mysql' ),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent'=> $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
],
];
$payload = array_filter( $payload['data'] ) ? $payload : [];
if ( empty( $payload ) ) {
return;
}
$args = [
'body' => wp_json_encode( $payload ),
'headers' => [
'Authorization' => 'Bearer ' . CF7_API_AUTH_TOKEN,
'Content-Type' => 'application/json',
'X-Request-ID' => wp_generate_uuid4(),
],
'timeout' => CF7_API_TIMEOUT,
'sslverify' => true,
];
$response = wp_remote_post( CF7_API_TARGET_URL, $args );
if ( is_wp_error( $response ) ) {
if ( CF7_API_LOG_LEVEL === 'debug' ) {
error_log( 'CF7 API Error: ' . $response->get_error_message() );
}
return;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
error_log( sprintf( 'CF7 API HTTP %d: %s', $code, wp_remote_retrieve_body( $response ) ) );
}
}, 10, 2 );
Quick Start Guide
- Create the dispatcher file: Save the configuration template as
cf7-api-dispatcher.php in your theme's root directory or wp-content/mu-plugins/.
- Update constants: Replace
CF7_API_TARGET_URL and CF7_API_AUTH_TOKEN with your actual endpoint and credentials. Adjust CF7_API_TIMEOUT based on your API's response characteristics.
- Map form fields: Modify the
$payload['data'] array to match your CF7 form field names (your-name, your-email, etc.). Use sanitize_* functions appropriate for each field type.
- Verify execution: Submit a test form. Check
wp-content/debug.log for CF7 API Success or CF7 API HTTP entries. If nothing appears, confirm the form ID matches the $allowed_forms array and that wpcf7_submit is firing.
- Monitor production: Route log entries to your monitoring stack (Datadog, Sentry, or CloudWatch). Set up alerts for HTTP 4xx/5xx responses or
WP_Error network failures to catch integration degradation before users report it.