Webhook Integration Guide

Connect your WordPress forms to n8n, Make, Zapier, and custom APIs to power your AI agents.

What is a Webhook?

Simple Explanation

Think of a webhook as a phone number for your software. When someone submits a form on your WordPress site, it "calls" this number and sends the form data. The receiver (n8n, Make, Zapier) processes the data and sends back a response.

User Submits Form
      ↓
WordPress packages data
      ↓
Sends POST request to webhook URL
      ↓
Webhook processes data (AI, database, email, etc.)
      ↓
Sends response back to WordPress
      ↓
User sees result

Technical Explanation

A webhook is an HTTP endpoint that:

  • Receives POST requests with JSON data
  • Processes the data (triggers AI, saves to database, sends emails, etc.)
  • Returns a JSON response
  • Operates asynchronously

Request Format

POST https://your-webhook-url.com/endpoint
Content-Type: application/json

{
  "agent_id": 123,
  "agent_name": "AI Image Generator",
  "form_data": {
    "field_description": "A futuristic city",
    "field_style": "Digital Art"
  },
  "timestamp": "2025-01-09 10:30:00",
  "user_id": 5
}

n8n Integration (Recommended)

Why n8n?

  • Open source - Free forever
  • Self-hosted - Full control over data
  • Visual editor - No coding required
  • 400+ integrations - Connect to anything
  • AI-friendly - Built-in OpenAI, Anthropic nodes

Setup n8n

Option 1: n8n Cloud (Easiest)

  1. Go to n8n.cloud
  2. Create free account
  3. Create new workflow
  4. Done! Your instance is ready

Option 2: Self-Hosted (Most Control)

# Using Docker
docker run -it --rm \
  --name n8n \
  -p 5678:5678 \
  -v ~/.n8n:/home/node/.n8n \
  n8nio/n8n

# Access at: http://localhost:5678

Create Your First Workflow

Step 1: Add Webhook Node

  1. Click "+" to add node
  2. Search for "Webhook"
  3. Select "Webhook" trigger
  4. Configure:
    • HTTP Method: POST
    • Path: /ai-agent
    • Response Mode: "Wait for Response"
  5. Copy the Production URL

Example URL: https://your-n8n.app.n8n.cloud/webhook/ai-agent

Step 2: Process the Data

Example: OpenAI Image Generation

  1. Add OpenAI node
  2. Connect it to Webhook
  3. Configure:
    • Resource: Image
    • Operation: Generate
    • Prompt: {{ $json.body.form_data.field_description }}
    • Size: {{ $json.body.form_data.field_size }}

Step 3: Format Response

Add Code node:

// Extract the generated image URL
const imageUrl = $json.data[0].url;

// Format response for WordPress
return {
  output_html: `
    <div class="ai-result">
      <h3>✨ Your AI Image is Ready!</h3>
      <img src="${imageUrl}" alt="Generated Image"
           style="max-width: 100%; border-radius: 10px;">
      <p><a href="${imageUrl}" download>Download Full Resolution</a></p>
    </div>
  `,
  image_url: imageUrl,
  success: true
};

Step 4: Return Response

  1. Add Respond to Webhook node
  2. Connect to Code node
  3. Configure:
    • Response Code: 200
    • Response Body: {{ $json }}

Step 5: Activate

  1. Click "Active" toggle in top-right
  2. Your workflow is live!

Complete n8n Workflows

Workflow 1: AI Chatbot

[Webhook Trigger]
    ↓
[Code: Extract message]
    ↓
[OpenAI: Chat completion]
    ↓
[Code: Format HTML response]
    ↓
[Respond to Webhook]

Response Formatting:

const reply = $json.choices[0].message.content;

return {
  output_html: `
    <div class="chat-response">
      <div class="ai-avatar">🤖</div>
      <div class="ai-message">${reply}</div>
    </div>
  `
};

Workflow 2: Document Analysis

[Webhook Trigger]
    ↓
[Code: Decode base64 file]
    ↓
[HTTP Request: Upload to storage]
    ↓
[OpenAI: Analyze document]
    ↓
[Send Email: Results]
    ↓
[Respond to Webhook]

Workflow 3: Lead to CRM

[Webhook Trigger]
    ↓
[Code: Format lead data]
    ↓
[HubSpot: Create contact]
    ↓
[Slack: Notify team]
    ↓
[Send Email: Confirmation]
    ↓
[Respond to Webhook]

Make (Integromat) Integration

Setup in Make

Step 1: Create Scenario

  1. Log into make.com
  2. Click "Create a new scenario"
  3. Search for "Webhooks"
  4. Select "Custom Webhook"

Step 2: Create Webhook

  1. Click "Add" next to webhook
  2. Name it: "WordPress AI Agent"
  3. Click "Save"
  4. Copy the webhook URL

Example: https://hook.us1.make.com/abc123xyz

Step 3: Add Modules

Example: OpenAI Integration

  1. Add OpenAI module
  2. Select "Create an Image"
  3. Map fields:
    • Prompt: {{body.form_data.field_description}}
    • Size: 1024x1024
  4. Add Webhook Response module
  5. Map response

Step 4: Activate

  1. Toggle "Scheduling" to ON
  2. Set to "Immediately"
  3. Click "Save"

Make Examples

  • Email Automation: Webhook → Gmail (Send Email) → Webhook Response
  • Database Storage: Webhook → Google Sheets (Add Row) → Webhook Response
  • Multi-Service: Webhook branches to Airtable, Slack, and Email, then responds

Zapier Integration

Setup in Zapier

Step 1: Create Zap

  1. Log into zapier.com
  2. Click "Create Zap"
  3. Search for "Webhooks by Zapier"
  4. Select "Catch Hook" trigger

Step 2: Get Webhook URL

  1. Copy the custom webhook URL
  2. It looks like: https://hooks.zapier.com/hooks/catch/123456/abc123/

Step 3: Test Webhook

  1. In WordPress, submit a test form
  2. Return to Zapier
  3. Click "Test trigger"
  4. You should see your form data

Step 4: Add Actions

Example: Send to Google Sheets

  1. Click "+" to add action
  2. Search "Google Sheets"
  3. Select "Create Spreadsheet Row"
  4. Map columns using Zapier variables

Returning Data to WordPress

Option A: Synchronous Response

Return finished HTML in the same webhook request. WordPress receives output_html and renders it immediately.

Required fields:

  • success - true or false
  • display_preview - short summary
  • output_html - the exact markup you want
{
  "success": true,
  "display_preview": "Upload received from user@example.com",
  "output_html": "<div class='ai-result-card'>...</div>"
}
💡 Tip: Keep synchronous responses under ~25 seconds and below 10 MB.

Option B: Deferred Callback

Zapier retired the old Webhook Response action, so the plugin now ships with its own authenticated callback endpoint. WordPress shows "Processing..." until the callback arrives.

  1. Trigger: Webhooks by Zapier → Catch Hook
  2. Immediate reply: Return { "success": true } quickly
  3. Processing steps: Run your AI calls, lookups, etc.
  4. Final action: POST back to the callback URL:
    • URL: map {{callback_url}}
    • Payload Type: json
    • Data: Include success, message, display_preview, output_html
⚠️ Zapier Limitations:
  • Built-in Webhook Response is missing → use callback POST
  • Requests longer than ~25 seconds will time out
  • Payloads larger than 10 MB are rejected
  • For real-time output, n8n or Make are better

Custom API Integration

Building Your Own Endpoint

Example in PHP

<?php
// webhook-endpoint.php

header('Content-Type: application/json');

// Get POST data
$json = file_get_contents('php://input');
$data = json_decode($json, true);

// Extract form data
$description = $data['form_data']['field_description'] ?? '';
$email = $data['form_data']['field_email'] ?? '';

// Process with your AI service
$result = callYourAIService($description);

// Return response
echo json_encode([
    'success' => true,
    'output_html' => '<div>' . htmlspecialchars($result) . '</div>',
    'processed_at' => date('Y-m-d H:i:s')
]);
?>

Example in Node.js

const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhook', async (req, res) => {
    const { form_data } = req.body;

    // Process with AI
    const result = await processWithAI(form_data.field_description);

    // Return response
    res.json({
        success: true,
        output_html: `<div class="result">${result}</div>`
    });
});

app.listen(3000, () => console.log('Webhook listening on port 3000'));

Example in Python (Flask)

from flask import Flask, request, jsonify
import openai

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    form_data = data.get('form_data', {})

    # Process with AI
    result = process_with_ai(form_data.get('field_description', ''))

    # Return response
    return jsonify({
        'success': True,
        'output_html': f'<div class="result">{result}</div>'
    })

if __name__ == '__main__':
    app.run(port=5000)

Understanding Webhook Data

Data Structure Sent to Webhook

{
  "agent_id": 123,
  "agent_name": "AI Image Generator",
  "form_data": {
    "field_description": "User's input here",
    "field_email": "user@example.com",
    "field_quantity": "3",
    "field_options": ["option1", "option2"],
    "field_image_file_data": "base64_encoded_string...",
    "field_image_file_name": "photo.jpg",
    "field_image_file_type": "image/jpeg",
    "field_image_file_size": 245678
  },
  "timestamp": "2025-01-09 10:30:45",
  "user_id": 5,
  "user_email": "logged-in-user@example.com"
}

Accessing Data in n8n

Text Field:

{{ $json.body.form_data.field_description }}

Select/Radio:

{{ $json.body.form_data.field_style }}

Multiple Values (checkboxes):

// In Code node
const options = $json.body.form_data.field_options;
// Returns: ["option1", "option2", "option3"]

File Upload:

// Base64 data
const fileData = $json.body.form_data.field_image_file_data;
const fileName = $json.body.form_data.field_image_file_name;
const fileType = $json.body.form_data.field_image_file_type;

// Convert to buffer if needed
const buffer = Buffer.from(fileData, 'base64');

Metadata:

Agent ID: {{ $json.body.agent_id }}
Agent Name: {{ $json.body.agent_name }}
Timestamp: {{ $json.body.timestamp }}
User ID: {{ $json.body.user_id }}

Understanding Webhook Responses (CRITICAL)

⚠️ THIS IS THE #1 SOURCE OF ISSUES! Your webhook MUST return the correct response format or nothing will display to users. Read this section carefully.

🔴 Common Mistake: Many users see "blank screen" or "no response" because their webhook returns the wrong format. This section explains EXACTLY what to return.

What WordPress Expects

When a user submits a form, WordPress sends data to your webhook and waits for a response. You MUST return:

  • Valid JSON (not HTML, not plain text)
  • HTTP Status 200 (success)
  • Content-Type: application/json header
  • ✅ A field containing HTML to display (output_html, output, or message)

The MUST-HAVE Response Structure

✅ Minimum Required (Recommended Format)

{
  "output_html": "<div>Your HTML content here</div>",
  "success": true
}

Explanation:

  • output_html - REQUIRED. The HTML that will be displayed to the user
  • success - Recommended. Set to true for successful responses
✅ This is all you need! If you return this format, WordPress will display your HTML to the user.

Complete Response Structure (All Fields)

{
  "success": true,
  "output_html": "<div class='ai-result'>Your response here</div>",
  "display_preview": "Short summary for admin dashboard",
  "message": "Optional success message",
  "data": {
    "any_additional": "data you want to include"
  }
}

Field Descriptions:

  • success - Boolean (true/false)
  • output_html - String containing HTML markup
  • display_preview - Optional short text for admin logs
  • message - Optional status message
  • data - Optional additional data (not displayed to user)

Accepted Response Formats (5 Options)

WordPress is flexible and accepts multiple formats. Here are ALL the formats that work:

Format 1: Standard (Recommended) ⭐

{
  "output_html": "<div class='result'>Your content here</div>",
  "success": true
}

When to use: Always. This is the clearest, most explicit format.

Format 2: Legacy Format

{
  "output": "<h3>Result</h3><p>Content here</p>",
  "success": true
}

When to use: If you're using older workflows. Uses output instead of output_html.

Format 3: OpenAI Message Format

{
  "message": {
    "content": "Your AI response here"
  }
}

When to use: When directly passing OpenAI chat response. WordPress extracts message.content and wraps it in HTML.

Format 4: Simple Message

{
  "message": "Processing complete!",
  "success": true
}

When to use: For simple text responses. WordPress wraps the message in <p> tags.

Format 5: Error Response

{
  "error": true,
  "message": "Something went wrong",
  "details": "Error details here"
}

When to use: When something fails. WordPress displays the error message to the user.

ℹ️ Auto-Normalization: WordPress automatically converts all these formats into a standard structure internally. You can use whichever format is easiest for your workflow.

Real-World Examples (Copy & Paste Ready)

Example 1: Simple Text Response

n8n Code Node:

return {
  output_html: `
    <div class="ai-response">
      <h3>✅ Success!</h3>
      <p>Your request has been processed.</p>
    </div>
  `,
  success: true
};

Example 2: AI Chatbot Response

n8n Code Node (after OpenAI):

// Get AI response from previous node
const aiReply = $json.choices[0].message.content;

return {
  output_html: `
    <div class="chat-response">
      <div class="avatar">🤖</div>
      <div class="message">${aiReply}</div>
    </div>
  `,
  success: true
};

Example 3: Image Generation Response

n8n Code Node (after DALL-E):

// Get generated image URL
const imageUrl = $json.data[0].url;

return {
  output_html: `
    <div class="image-result">
      <h3>🎨 Your AI Image is Ready!</h3>
      <img src="${imageUrl}" alt="Generated Image"
           style="max-width: 100%; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
      <p>
        <a href="${imageUrl}" download class="download-btn">
          Download Full Resolution
        </a>
      </p>
    </div>
  `,
  success: true
};

Example 4: Multiple Results

n8n Code Node:

const formData = $json.body.form_data;
const userEmail = formData.field_email;
const description = formData.field_description;

return {
  output_html: `
    <div class="result-card">
      <h3>📊 Processing Complete</h3>

      <div class="info-section">
        <strong>Email:</strong> ${userEmail}
      </div>

      <div class="info-section">
        <strong>Your Request:</strong> ${description}
      </div>

      <div class="info-section">
        <strong>Status:</strong> ✅ Complete
      </div>

      <p style="margin-top: 20px; padding: 15px; background: #f0f9ff; border-radius: 8px;">
        We've sent detailed results to your email address.
      </p>
    </div>
  `,
  success: true,
  display_preview: `Request from ${userEmail}`
};

Example 5: Error Handling

n8n Code Node (error scenario):

const formData = $json.body.form_data;
const description = formData.field_description;

// Validate input
if (!description || description.length < 10) {
  return {
    output_html: `
      <div class="error-message">
        <h3>⚠️ Invalid Input</h3>
        <p>Please provide a description of at least 10 characters.</p>
        <p>You entered: ${description ? description.length : 0} characters.</p>
      </div>
    `,
    success: false,
    error: true,
    message: "Description too short"
  };
}

// Process normally if valid
return {
  output_html: `<div>Processing...</div>`,
  success: true
};

Example 6: With Dynamic Data

n8n Code Node (using form data):

const formData = $json.body.form_data;
const topic = formData.field_topic || 'general topic';
const tone = formData.field_tone || 'professional';
const length = formData.field_length || '500';

// Simulate AI processing (replace with actual AI call)
const content = `This is your ${tone} content about ${topic}, approximately ${length} words.`;

return {
  output_html: `
    <div class="content-result">
      <h3>✨ Your AI Content</h3>

      <div class="metadata">
        <span><strong>Topic:</strong> ${topic}</span> |
        <span><strong>Tone:</strong> ${tone}</span> |
        <span><strong>Length:</strong> ~${length} words</span>
      </div>

      <div class="content-body" style="margin-top: 20px; padding: 20px; background: white; border-radius: 8px;">
        ${content}
      </div>

      <button onclick="navigator.clipboard.writeText(this.previousElementSibling.textContent)"
              style="margin-top: 15px; padding: 10px 20px; background: #3a79f7; color: white; border: none; border-radius: 5px; cursor: pointer;">
        📋 Copy to Clipboard
      </button>
    </div>
  `,
  success: true
};

Common Response Mistakes & Fixes

❌ WRONG: Returning Plain Text

// This will NOT work
return "Your result here";

Fix:

// ✅ Correct
return {
  output_html: "<div>Your result here</div>",
  success: true
};

❌ WRONG: Missing Quotes in JSON

// Invalid JSON - will cause errors
return {
  output_html: <div>Result</div>,  // Missing quotes!
  success: true
};

Fix:

// ✅ Correct
return {
  output_html: "<div>Result</div>",  // Proper quotes
  success: true
};

❌ WRONG: HTML Without Wrapper

// Not wrapped in object
return "<div>Result</div>";

Fix:

// ✅ Correct
return {
  output_html: "<div>Result</div>",
  success: true
};

❌ WRONG: Forgetting to Escape HTML

// Unescaped quotes break JSON
return {
  output_html: "<div class="result">Text</div>",  // Breaks!
  success: true
};

Fix:

// ✅ Correct - Use single quotes in HTML
return {
  output_html: "<div class='result'>Text</div>",
  success: true
};

// Or use template literals
return {
  output_html: `<div class="result">Text</div>`,
  success: true
};

❌ WRONG: Not Setting Content-Type

In custom API endpoints, you MUST set the header:

// PHP
header('Content-Type: application/json');

// Node.js (Express)
res.setHeader('Content-Type', 'application/json');

// Python (Flask)
return jsonify({...})

Testing Your Response Format

Step 1: Check Response in n8n

  1. Run your workflow in n8n
  2. Click the last node (Respond to Webhook)
  3. Check "Output" tab
  4. Verify you see: {"output_html": "...", "success": true}

Step 2: Check Response in Browser

  1. Submit form on WordPress
  2. Press F12 → Network tab
  3. Find the admin-ajax.php request
  4. Click it → Response tab
  5. You should see your JSON response

Step 3: Validate JSON

  1. Copy your response from browser
  2. Go to JSONLint.com
  3. Paste and click "Validate JSON"
  4. Fix any syntax errors shown

Response Troubleshooting Guide

🔴 Problem: Blank Screen / No Response Shows

Causes:

  • Missing output_html field
  • Invalid JSON syntax
  • HTTP status not 200
  • Content-Type not application/json

Debug Steps:

  1. Open browser DevTools (F12)
  2. Go to Console tab - check for JavaScript errors
  3. Go to Network tab - find admin-ajax.php request
  4. Check Response tab - verify JSON is valid
  5. Check Headers tab - verify Content-Type is application/json
  6. Check Status - should be 200

Quick Fix:

// In n8n Code node, use this template:
return {
  output_html: `<div>Test response</div>`,
  success: true
};

🔴 Problem: "Unexpected token < in JSON"

Cause: Webhook returned HTML instead of JSON (usually an error page)

Fix:

  1. Check n8n execution log for errors
  2. Verify workflow is active
  3. Make sure Respond to Webhook node returns JSON
  4. Check for 404/500 errors

🔴 Problem: Shows "Processing..." Forever

Causes:

  • Webhook timeout (> 60 seconds)
  • Network connection lost
  • Workflow not returning response

Fix:

  1. Check workflow execution time in n8n
  2. Simplify workflow to return faster
  3. Add timeout handling in workflow
  4. Use async processing for long tasks

🔴 Problem: HTML Shows as Raw Text

Cause: HTML is being escaped/encoded

Fix: Ensure you're returning raw HTML in output_html, not escaped:

// ✅ Correct
output_html: "<div>Hello</div>"

// ❌ Wrong (escaped)
output_html: "&lt;div&gt;Hello&lt;/div&gt;"

Response Best Practices

💡 Best Practices:

  • ✅ Always include success: true for successful responses
  • ✅ Use template literals (backticks) for multi-line HTML
  • ✅ Include inline styles for consistent appearance
  • ✅ Add success/error icons (✅ ❌ ⚠️) for visual feedback
  • ✅ Return user-friendly error messages, not technical errors
  • ✅ Keep responses under 1MB (large images = slow)
  • ✅ Test with empty/invalid inputs
  • ✅ Log errors for debugging

Advanced: Custom Styling in Responses

Add inline CSS for beautiful responses:

return {
  output_html: `
    <div style="
      max-width: 600px;
      margin: 20px auto;
      padding: 30px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      border-radius: 15px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.2);
      color: white;
      font-family: system-ui, -apple-system, sans-serif;
    ">
      <h2 style="margin: 0 0 15px 0; font-size: 24px;">
        ✨ AI Result
      </h2>
      <p style="font-size: 16px; line-height: 1.6; margin: 0;">
        Your content here...
      </p>
    </div>
  `,
  success: true
};
✅ Summary: The webhook response is THE most critical part. Always return valid JSON with an output_html field containing your HTML. Test thoroughly!

Common Integrations

1. OpenAI GPT (Text Generation)

n8n Workflow: Webhook → OpenAI Chat → Code (Format) → Respond

Configuration:

  • Model: gpt-4 or gpt-3.5-turbo
  • Temperature: 0.7 (creative) or 0.3 (focused)
  • Max Tokens: 500-2000

2. DALL-E (Image Generation)

n8n Workflow: Webhook → OpenAI Image → Code (Format) → Respond

Configuration:

  • Size: 1024x1024, 1024x1792, or 1792x1024
  • Quality: HD or Standard
  • n: Number of images (1-4)

3. Anthropic Claude

n8n Workflow: Webhook → HTTP Request (Anthropic API) → Code (Parse) → Respond

4. Email with SendGrid

n8n Workflow: Webhook → SendGrid (Send Email) → Respond

5. Save to Airtable

n8n Workflow: Webhook → Airtable (Create Record) → Respond

6. Slack Notification

n8n Workflow: Webhook → Slack (Send Message) → Respond

Troubleshooting

Webhook Not Receiving Data

Check:

  1. ✅ Webhook URL is correct
  2. ✅ Workflow is activated
  3. ✅ No typos in URL
  4. ✅ Webhook accepts POST requests
  5. ✅ Check n8n execution history

Test with cURL:

curl -X POST https://your-webhook-url.com/endpoint \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": 123,
    "form_data": {
      "field_test": "hello"
    }
  }'

Timeout Errors

Causes:

  • Webhook takes > 60 seconds
  • AI service is slow
  • Network issues

Solutions:

  1. Optimize workflow - Remove unnecessary steps
  2. Use async processing - Return immediate response, email results
  3. Show loading state - Let users know it's processing

Response Not Showing

Check:

  1. Response format is correct JSON
  2. Contains output_html or output field
  3. Status code is 200
  4. No JavaScript errors in console

Debug in Browser:

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Submit form
  4. Find wp-admin/admin-ajax.php request
  5. Check response

WordPress shows "Webhook error 4xx/5xx"

Starting with the 11 Nov 2025 update, the form surfaces the exact HTTP status and the first part of the webhook response.

Common errors:

  • Webhook error 410: Scenario inactive → Turn the scenario ON or update the webhook URL
  • Webhook error 500: Unexpected token... → Check for malformed JSON in response
  • Webhook error 401/403 → Check auth headers or shared secrets
✅ Solution: Once the webhook responds with HTTP 200 and valid JSON, the form will render the HTML properly.

Best Practices

Security

✅ DO:

  • Use HTTPS webhooks only
  • Validate all input data
  • Sanitize before using in AI prompts
  • Rate limit webhook calls
  • Log all requests

❌ DON'T:

  • Expose API keys in responses
  • Trust user input blindly
  • Store sensitive data unencrypted

Performance

✅ Optimize:

  • Keep workflows simple
  • Cache common responses
  • Use async processing for slow tasks
  • Return responses quickly (<3s ideal)

User Experience

✅ Provide:

  • Clear loading states
  • Helpful error messages
  • Success confirmations
  • Progress indicators for long tasks