Any PHP website can receive PostPenguin webhooks. This guide shows you how to create a simple webhook endpoint that saves posts to your database.
Perfect for: Custom PHP sites, Laravel, Symfony, CodeIgniter, or any PHP-based CMS.
๐ Quick Setup
Step 1: Create Webhook Endpoint
Create a new PHP file at /api/webhooks/postpenguin.php (or any accessible path):
<?php
// PostPenguin Webhook Handler
// Place this file at: /api/webhooks/postpenguin.php
header('Content-Type: application/json');
// Your webhook secret (set this to match PostPenguin)
$webhookSecret = getenv('POSTPENGUIN_WEBHOOK_SECRET') ?: 'your-secret-key';
// Get raw POST data
$rawData = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_POSTPENGUIN_SIGNATURE'] ?? '';
// Validate request
if (empty($rawData)) {
http_response_code(400);
echo json_encode(['error' => 'No data received']);
exit;
}
// Verify signature (recommended)
if (!empty($signature)) {
$expectedSignature = hash_hmac('sha256', $rawData, $webhookSecret);
$receivedSignature = str_replace('sha256=', '', $signature);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
}
// Parse JSON data
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Validate required fields
$requiredFields = ['title', 'slug', 'html'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
http_response_code(400);
echo json_encode(['error' => "Missing required field: $field"]);
exit;
}
}
// Extract post data
$post = [
'postpenguin_id' => $data['postPenguinId'] ?? uniqid('pp_'),
'title' => htmlspecialchars($data['title'], ENT_QUOTES, 'UTF-8'),
'slug' => preg_replace('/[^a-z0-9-]/', '', strtolower($data['slug'])),
'html' => $data['html'], // Sanitize as needed for your use case
'meta_title' => htmlspecialchars($data['meta_title'] ?? $data['title'], ENT_QUOTES, 'UTF-8'),
'meta_description' => htmlspecialchars($data['meta_description'] ?? '', ENT_QUOTES, 'UTF-8'),
'featured_image' => filter_var($data['featured_image'] ?? '', FILTER_VALIDATE_URL) ?: null,
'published_at' => date('Y-m-d H:i:s'),
];
// TODO: Save to your database (example below)
// $postId = saveToDatabase($post);
// For demo: just log and return success
error_log("PostPenguin webhook received: " . $post['title']);
http_response_code(200);
echo json_encode([
'success' => true,
'postId' => $post['postpenguin_id'],
'message' => 'Post received successfully'
]);
Step 2: Add Database Storage
Add this function to save posts to MySQL/PostgreSQL:
<?php
// Database configuration
$dbHost = getenv('DB_HOST') ?: 'localhost';
$dbName = getenv('DB_NAME') ?: 'your_database';
$dbUser = getenv('DB_USER') ?: 'your_username';
$dbPass = getenv('DB_PASS') ?: 'your_password';
function saveToDatabase($post) {
global $dbHost, $dbName, $dbUser, $dbPass;
try {
$pdo = new PDO(
"mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4",
$dbUser,
$dbPass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Check if post exists
$stmt = $pdo->prepare("SELECT id FROM posts WHERE postpenguin_id = ?");
$stmt->execute([$post['postpenguin_id']]);
$existing = $stmt->fetch();
if ($existing) {
// Update existing post
$stmt = $pdo->prepare("
UPDATE posts SET
title = ?,
slug = ?,
html = ?,
meta_title = ?,
meta_description = ?,
featured_image = ?,
updated_at = NOW()
WHERE postpenguin_id = ?
");
$stmt->execute([
$post['title'],
$post['slug'],
$post['html'],
$post['meta_title'],
$post['meta_description'],
$post['featured_image'],
$post['postpenguin_id']
]);
return $existing['id'];
} else {
// Insert new post
$stmt = $pdo->prepare("
INSERT INTO posts
(postpenguin_id, title, slug, html, meta_title, meta_description, featured_image, published_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
");
$stmt->execute([
$post['postpenguin_id'],
$post['title'],
$post['slug'],
$post['html'],
$post['meta_title'],
$post['meta_description'],
$post['featured_image'],
$post['published_at']
]);
return $pdo->lastInsertId();
}
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
throw new Exception("Database error");
}
}
Step 3: Create Database Table
Run this SQL to create the posts table:
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
postpenguin_id VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(500) NOT NULL,
slug VARCHAR(255) NOT NULL,
html TEXT NOT NULL,
meta_title VARCHAR(500),
meta_description TEXT,
featured_image VARCHAR(1000),
published_at DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_slug (slug),
INDEX idx_published_at (published_at)
);Step 4: Configure PostPenguin
When adding your site to PostPenguin:
- Webhook URL:
https://your-site.com/api/webhooks/postpenguin.php - Secret Key: Set a secure random string (same as
POSTPENGUIN_WEBHOOK_SECRET)
๐ Display Posts on Your Site
Create a simple blog page to display your posts:
<?php
// blog.php - Display posts from database
$pdo = new PDO("mysql:host=localhost;dbname=your_db", "user", "pass");
// Fetch published posts
$stmt = $pdo->query("
SELECT * FROM posts
ORDER BY published_at DESC
LIMIT 10
");
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Latest Posts</h1>
<?php foreach ($posts as $post): ?>
<article>
<?php if ($post['featured_image']): ?>
<img src="<?= htmlspecialchars($post['featured_image']) ?>" alt="<?= htmlspecialchars($post['title']) ?>">
<?php endif; ?>
<h2 className="text-gray-900">
<a href="/post/<?= htmlspecialchars($post['slug']) ?>">
<?= htmlspecialchars($post['title']) ?>
</a>
</h2>
<p className="text-gray-700"><?= htmlspecialchars($post['meta_description']) ?></p>
<time><?= date('F j, Y', strtotime($post['published_at'])) ?></time>
</article>
<?php endforeach; ?>
</body>
</html>๐งช Testing
Test Your Webhook
# Test with curl
curl -X POST https://your-site.com/api/webhooks/postpenguin.php \
-H "Content-Type: application/json" \
-d '{
"postPenguinId": "test_123",
"title": "Test Post",
"slug": "test-post",
"html": "<p className="text-gray-700">This is a test post from PostPenguin.</p>",
"meta_title": "Test Post Title",
"meta_description": "Testing PHP webhook integration",
"featured_image": "https://via.placeholder.com/800x400",
"status": "publish"
}'Test with Signature
# Generate signature and test
SECRET="your-webhook-secret"
PAYLOAD='{"title":"Test","slug":"test","html":"<p className="text-gray-700">Test</p>"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
curl -X POST https://your-site.com/api/webhooks/postpenguin.php \
-H "Content-Type: application/json" \
-H "X-PostPenguin-Signature: sha256=$SIGNATURE" \
-d "$PAYLOAD"๐ Security Checklist
- Always verify webhook signatures with HMAC-SHA256
- Use HTTPS for your webhook endpoint
- Store secrets in environment variables, not in code
- Use prepared statements to prevent SQL injection
- Sanitize HTML content before displaying
๐ Troubleshooting
Webhook Not Receiving Posts
- Check your webhook URL is publicly accessible
- Verify the URL doesn't have typos in PostPenguin settings
- Check PHP error logs:
tail -f /var/log/php/error.log - Ensure your server allows POST requests to the endpoint
Signature Verification Failing
- Ensure your secret key matches PostPenguin's configuration
- Check that you're using the raw request body for signature calculation
- Verify the signature header name is correct:
X-PostPenguin-Signature
Database Errors
- Check database credentials are correct
- Ensure the posts table exists with correct schema
- Verify database user has INSERT/UPDATE permissions
Need Help?
Check our webhook documentation for technical details, or contact support for custom integrations.