Skip to content

Webhook Integration

Connect any custom CMS to BlogShoot using standard webhooks. Receive AI-generated articles automatically through secure HTTP callbacks.

Webhook integration allows you to receive BlogShoot articles in any custom CMS or platform that can handle HTTP POST requests. Instead of manually copying content, BlogShoot automatically pushes articles to your webhook endpoint when they’re ready.

BlogShoot generates article → You select Webhook push →
BlogShoot sends HTTP POST → Your CMS receives and processes
  • Standard HTTP webhooks - Works with any platform that accepts POST requests
  • Secure delivery - HMAC-SHA256 signature verification prevents forgery
  • Automatic retries - Failed deliveries retry up to 3 times automatically
  • Custom headers - Add authentication tokens or API keys
  • Delivery history - Track all webhook deliveries and troubleshoot issues

Before setting up webhook integration, you’ll need:

  • A publicly accessible HTTPS endpoint (webhook URL)
  • Ability to receive and process HTTP POST requests
  • Basic understanding of JSON and API authentication
  • Development resources to implement the webhook receiver

First, create an endpoint in your CMS that can receive HTTP POST requests. This endpoint should:

  • Accept POST requests with JSON payload
  • Return 2xx status code (200-299) on success
  • Process requests quickly (respond within 10 seconds)
  • Verify webhook signatures (see security section)
  1. Navigate to IntegrationsWebhook
  2. Click “Add Webhook Connection”
  3. Fill in connection details:
Connection Name: My Custom CMS
Webhook URL: https://your-cms.com/webhooks/blogshoot
Webhook Secret: [generate a strong random string]
  1. (Optional) Add custom HTTP headers:

    • Click “Add Custom Header”
    • Example: Authorization: Bearer your-api-token
  2. Click “Test Connection”

    • ✅ Success: Connection verified
    • ❌ Failed: Check troubleshooting section
  3. Click “Save Connection”

Configure which events trigger webhooks:

  • article.created - New article published
  • article.updated - Existing article modified
  • article.deleted - Article removed

You can also set:

  • Timeout - Maximum wait time for response (default: 10 seconds)
  • Custom Headers - Additional authentication or metadata

BlogShoot sends a structured JSON payload containing all article data:

{
"event_id": "unique-uuid-v4",
"event_type": "article.created",
"timestamp": "2025-12-04T10:30:00Z",
"workspace": {
"id": "workspace_id",
"name": "My Company"
},
"article": {
"id": "blogshoot_article_id",
"title": "Your Article Title",
"slug": "article-url-slug",
"content": {
"html": "<p>Full HTML content...</p>",
"markdown": "# Article Title\n\nContent...",
"plain_text": "Plain text version"
},
"excerpt": "Brief article summary",
"featured_image": {
"url": "https://cdn.blogshoot.com/image.jpg",
"alt": "Image description",
"width": 1200,
"height": 630
},
"images": [
{
"url": "https://...",
"alt": "Image description",
"position": 1
}
],
"seo": {
"title": "SEO-optimized title",
"description": "Meta description",
"keywords": ["keyword1", "keyword2"]
},
"categories": ["Category1", "Category2"],
"tags": ["tag1", "tag2"],
"author": {
"name": "Author Name",
"email": "[email protected]"
},
"status": "publish",
"published_at": "2025-12-04T10:30:00Z",
"created_at": "2025-12-04T10:00:00Z",
"updated_at": "2025-12-04T10:30:00Z"
},
"metadata": {
"source": "blogshoot",
"version": "1.0"
}
}

Each webhook request includes these headers:

Content-Type: application/json
User-Agent: BlogShoot-Webhook/1.0
X-BlogShoot-Signature: [hmac-sha256-hex-signature]
X-BlogShoot-Event: article.created
X-BlogShoot-Delivery-ID: [unique-uuid]

Always verify webhook signatures to ensure requests come from BlogShoot:

<?php
// Read raw request body
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_BLOGSHOOT_SIGNATURE'] ?? '';
// Your webhook secret from BlogShoot dashboard
$webhook_secret = 'your-webhook-secret-from-blogshoot';
// Calculate expected signature
$expected_signature = hash_hmac('sha256', $payload, $webhook_secret);
// Verify signature
if (!hash_equals($expected_signature, $signature)) {
http_response_code(401);
die(json_encode(['error' => 'Invalid signature']));
}
// Signature verified - safe to process
$data = json_decode($payload, true);
processWebhook($data);
?>
const crypto = require('crypto');
app.post('/webhook/blogshoot', (req, res) => {
// Get signature from header
const signature = req.headers['x-blogshoot-signature'];
const webhookSecret = process.env.BLOGSHOOT_WEBHOOK_SECRET;
// Calculate expected signature from raw body
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(req.rawBody) // Must use raw body!
.digest('hex');
// Verify signature
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
processWebhook(req.body);
res.status(200).json({ success: true });
});

Handle different event types:

<?php
$data = json_decode($payload, true);
switch ($data['event_type']) {
case 'article.created':
createPost($data['article']);
break;
case 'article.updated':
updatePost($data['article']);
break;
case 'article.deleted':
deletePost($data['article']['id']);
break;
case 'test':
// Test connection - just return success
break;
}
// Return 200 OK
http_response_code(200);
echo json_encode([
'success' => true,
'event_id' => $data['event_id']
]);
?>

Articles include a featured image and additional images. You can:

  1. Store URLs directly - Reference BlogShoot’s CDN
  2. Download and host - Upload images to your own storage
function downloadImage($url, $alt = '') {
$image_data = file_get_contents($url);
$filename = basename(parse_url($url, PHP_URL_PATH));
$filepath = '/uploads/' . $filename;
file_put_contents($filepath, $image_data);
return [
'path' => $filepath,
'alt' => $alt
];
}
// Process featured image
if (!empty($article['featured_image']['url'])) {
$image = downloadImage(
$article['featured_image']['url'],
$article['featured_image']['alt']
);
setFeaturedImage($post_id, $image);
}

If webhook delivery fails, BlogShoot automatically retries:

AttemptDelayTotal Time
1st failure5 seconds5s
2nd failure30 seconds35s
3rd failure5 minutes5m 35s
After 3 failuresManual retry needed-

Delivery is considered successful when:

  • HTTP status code is 200-299
  • Response received within timeout (default: 10 seconds)

For permanently failed deliveries:

  1. Go to IntegrationsWebhook
  2. Click “View Delivery History”
  3. Find the failed delivery
  4. Click “Retry”

Track all webhook deliveries:

  1. Navigate to IntegrationsWebhook
  2. Select your connection
  3. Click “Delivery History”

You’ll see:

  • Event type and timestamp
  • Delivery status (Success/Failed)
  • HTTP status code
  • Response time
  • Error messages (if failed)
  • Retry count
  • Success ✅ - Delivered successfully
  • Pending ⏳ - Scheduled for retry
  • Failed ❌ - Permanent failure (needs manual retry)
  1. Respond quickly - Process webhook asynchronously if possible:

    // Respond immediately
    http_response_code(200);
    echo json_encode(['success' => true]);
    fastcgi_finish_request(); // PHP-FPM only
    // Then process in background
    processArticleInBackground($data);
  2. Set appropriate timeout - Match your CMS processing speed:

    • Simple insert: 5-10 seconds
    • Complex processing: 15-30 seconds
    • Image downloads: 20-60 seconds
  1. Always verify signatures - Never trust unsigned webhooks
  2. Use HTTPS - Encrypt data in transit
  3. Store secrets securely - Use environment variables
  4. Rotate secrets periodically - Every 6-12 months
  5. Add custom auth headers - Additional authentication layer
  1. Implement idempotency - Use event_id to prevent duplicates:

    $event_id = $data['event_id'];
    if (eventAlreadyProcessed($event_id)) {
    http_response_code(200);
    die(json_encode(['success' => true, 'duplicate' => true]));
    }
    markEventProcessed($event_id);
  2. Log all webhooks - Keep audit trail for debugging

  3. Handle errors gracefully - Return 5xx for temporary failures

  4. Monitor delivery history - Set up alerts for failures

  1. Click “Test Connection” in BlogShoot dashboard
  2. BlogShoot sends a test event: { "event_type": "test", ... }
  3. Your endpoint should:
    • Verify signature
    • Return 200 OK
    • Log the test event

Before connecting to BlogShoot, test your endpoint with:

  • Webhook.site - Inspect incoming requests
  • RequestBin - Capture and debug webhooks
  • Postman - Send test POST requests manually
Terminal window
curl -X POST https://your-cms.com/webhooks/blogshoot \
-H "Content-Type: application/json" \
-H "X-BlogShoot-Signature: [calculated-signature]" \
-H "X-BlogShoot-Event: test" \
-d '{
"event_type": "test",
"event_id": "test-123",
"timestamp": "2025-12-04T10:30:00Z"
}'

Possible causes:

  1. Endpoint not accessible

    • Verify URL is publicly accessible
    • Check firewall rules
    • Ensure HTTPS is configured
  2. Signature verification failed

    • Check webhook secret matches
    • Verify you’re using raw request body
    • Check for encoding issues
  3. Timeout

    • Increase timeout setting
    • Optimize endpoint response time
    • Implement async processing
  4. Invalid response

    • Ensure 200-299 status code
    • Return valid JSON
    • Check for error handling

Troubleshooting steps:

  1. Check delivery history for error messages
  2. Verify endpoint logs - Is request received?
  3. Test signature - Calculate locally and compare
  4. Check response time - Too slow? Optimize or increase timeout
  5. Review error logs - Server errors, database issues?
  1. Check image URLs are accessible
  2. Verify sufficient storage space
  3. Check file permissions on upload directory
  4. Ensure firewall allows outbound HTTPS

Implement idempotency using event_id:

// Check if already processed
$event_id = $data['event_id'];
if (eventExists($event_id)) {
return ['success' => true, 'duplicate' => true];
}
// Process and mark as handled
processArticle($data['article']);
saveEventId($event_id);

Add custom headers for additional security:

Authorization: Bearer your-api-token
X-API-Key: your-api-key
X-Custom-Header: custom-value

Access in your endpoint:

$api_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($api_key !== 'your-api-key') {
http_response_code(401);
die('Unauthorized');
}

Currently, all article data is sent. If you need specific fields only, process the payload and extract what you need:

function processArticle(article) {
// Extract only what you need
return {
title: article.title,
content: article.content.html,
excerpt: article.excerpt,
featured_image: article.featured_image?.url,
categories: article.categories,
tags: article.tags
};
}

You can create multiple webhook connections:

  • Development - Test endpoint for staging
  • Production - Live CMS endpoint
  • Backup - Secondary CMS or archive system

Each connection has independent:

  • URL and secret
  • Event configuration
  • Custom headers
  • Delivery history
  • Never commit secrets to version control
  • Use environment variables or secret management systems
  • Rotate secrets regularly every 6-12 months
  • Use different secrets for dev/staging/production

For additional security, whitelist BlogShoot’s IP addresses:

Contact [email protected] for current IP ranges

BlogShoot respects reasonable rate limits:

  • Maximum 5 webhooks per second per connection
  • Exponential backoff on errors
  • Automatic retry with delays

If you need higher limits, contact support.