Articles on: Content Publishing and Scheduling

How to setup Webhooks integration with Contentpen

What are Webhooks?

Webhooks let you receive automatic notifications when events happen in Contentpen. Instead of checking back to see if your blog post is ready, we'll send a request to your server the moment it's done.

Currently supported events:

  • Blog post generated - When your article is successfully created
  • Blog post failed - When something goes wrong during generation


How to Setup


1) Create a Webhook Endpoint

First, you'll need a URL on your server that can receive POST requests from Contentpen. This is where we'll send event notifications.

Your endpoint should:

  • Accept POST requests
  • Return a 2xx status code within 10 seconds
  • Use HTTPS (required)


2) Add the Webhook in Contentpen

Go to your Integrations page and click on Webhooks.


Click Add Webhook to create a new endpoint.


Fill in the details:

  • Endpoint URL - Your endpoint URL (must be HTTPS)
  • Description - Optional, helps you identify this webhook later
  • Events - Select which events should trigger this webhook




Click Create Webhook and you'll see your signing secret.

⚠️ Important: Copy and save your signing secret now. It will only be shown once. You'll need it to verify webhook signatures.


Done! Your webhook is now active and will start receiving events.

Verifying Webhook Signatures


Every webhook request includes a signature so you can verify it actually came from Contentpen. Always verify signatures before processing webhooks.


Signature Header

We send the signature in the X-Contentpen-Signature header:


X-Contentpen-Signature: t=1705315800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t - Unix timestamp when we signed the request
  • v1 - The HMAC SHA-256 signature


Python Verification Example

import hmac
import hashlib
import time

def verify_contentpen_signature(payload: str, signature_header: str, secret: str, tolerance: int = 300) -> bool:
"""
Verify a Contentpen webhook signature.

Args:
payload: Raw request body as string
signature_header: Value of X-Contentpen-Signature header
secret: Your webhook signing secret (starts with whsec_)
tolerance: Max age in seconds (default 5 minutes)

Returns:
True if valid, raises ValueError otherwise
"""
# Parse the signature header
parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = int(parts["t"])
received_sig = parts["v1"]

# Reject old requests (replay protection)
if abs(time.time() - timestamp) > tolerance:
raise ValueError("Timestamp too old")

# Compute expected signature
signing_key = secret.replace("whsec_", "")
expected_sig = hmac.new(
signing_key.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256
).hexdigest()

# Compare signatures (constant-time to prevent timing attacks)
if not hmac.compare_digest(expected_sig, received_sig):
raise ValueError("Invalid signature")

return True


Flask Example

from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"

@app.route("/webhooks/contentpen", methods=["POST"])
def handle_webhook():
payload = request.get_data(as_text=True)
signature = request.headers.get("X-Contentpen-Signature")

try:
verify_contentpen_signature(payload, signature, WEBHOOK_SECRET)
except ValueError as e:
abort(401, str(e))

# Process the webhook
data = request.json
event = data["meta"]["event_type"]

if event == "blog_post.generation_completed":
# Handle successful generation
blog_post = data["data"]["blog_post"]
print(f"Blog ready: {blog_post['title']}")

elif event == "blog_post.generation_failed":
# Handle failed generation
error_message = data["data"]["error_message"]
print(f"Generation failed: {error_message}")

return {"received": True}


Event Payloads


blog_post.generation_completed

Sent when your blog post is successfully generated.

 

Sample Payload

{
"data": {
"author": {
"id": "be3f0498-0381-46fb-8682-52abf1fff9b8",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe"
},
"blog_post": {
"id": "cea66be7-d8a4-47fa-9f45-3d753020de64",
"slug": "islamic-car-financing-pakistan-guide",
"title": "Islamic Car Financing in Pakistan: A Complete Guide",
"topic": "Getting Islamic Car Financing in Pakistan",
"keyword": "Islamic Car Financing",
"outline": "<h1>Blog Outline...</h1>",
"language": "en-US",
"created_at": "2025-12-18T12:41:17.372587",
"updated_at": "2025-12-18T12:50:38.220962",
"meta_title": "Islamic Car Financing in Pakistan: A Complete Guide",
"meta_description": "Islamic car financing explained for Pakistan...",
"word_count": 2186,
"article_size": "small",
"html_content": "<h2>Introduction</h2><p>Your article content...</p>",
"markdown_content": null,
"featured_image_url": "https://example.com/image.jpg",
"featured_image_alt": null,
"secondary_keywords": null
},
"generation_type": "one_shot",
"duration_seconds": null
},
"meta": {
"event_id": "351ea43e-1878-4b08-9118-18235717e699",
"event_type": "blog_post.generation_completed",
"event_version": "1.0",
"timestamp": "2025-12-18T12:50:39.272448Z",
"workspace_id": "a491fc0c-a961-481d-8939-b2a2e02b175e",
"organization_id": "fec10d37-8178-41f3-8e83-6aebff170d46",
"triggered_by": "be3f0498-0381-46fb-8682-52abf1fff9b8"
}
}


blog_post.generation_failed

Sent when blog post generation fails.

 

Sample Payload

{
"data": {
"blog_post": {
"id": "32390d6a-8a6b-4dcf-a3c0-c3875327901c",
"slug": "...",
"title": "...",
"topic": "Impacts of AI on Everyday Life",
"keyword": "AI",
"language": "en-US",
"created_at": "2025-12-19T04:56:21.396444",
"updated_at": "2025-12-19T04:56:51.776592",
"article_size": "medium"
},
"error_code": "GENERATION_ERROR",
"error_message": "SERP analysis failed to return results",
"generation_type": "one_shot"
},
"meta": {
"event_id": "d925551f-7ff7-4a1f-8ab9-e1e20313848b",
"timestamp": "2025-12-19T04:56:51.941889Z",
"event_type": "blog_post.generation_failed",
"triggered_by": "b8b7b29b-66da-42aa-b8c5-3b0889418a09",
"workspace_id": "b5c01989-5f4d-4f4a-a029-e8e92ed075d0",
"event_version": "1.0",
"organization_id": "699ddf8b-cfd7-4782-8cf7-695d8db1b01c"
}
}


HTTP Headers

Every webhook request includes these headers:

Header

Description

Content-Type

application/json

User-Agent

Contentpen-Webhook/1.0

X-Contentpen-Event

Event type (e.g., blog_post.generation_completed)

X-Contentpen-Delivery-Id

Unique delivery ID (use for deduplication)

X-Contentpen-Signature

HMAC signature for verification

X-Contentpen-Timestamp

Unix timestamp


Retries

If your endpoint returns an error (non-2xx status) or times out, we'll retry the delivery:

Attempt

Delay

1st retry

1 minute

2nd retry

5 minutes

3rd retry

30 minutes

4th retry

2 hours

After 4 failed retries, the delivery is marked as failed.


Testing Your Webhook

You can send a test event to verify your endpoint is working.

From the webhook endpoint menu, click the menu on your webhook endpoint and click Ping test.

This sends a ping event to your endpoint:

{
"event": "ping",
"created_at": "2025-12-19 06:17:31.713981+00:00",
"data": {
"message": "This is a test webhook from ContentPen",
"workspace_id": "b5c01989-5f4d-4f4a-a029-e8e92ed075d0",
"endpoint_id": "cc4b2837-f216-41e3-aece-a35b2f6987ca"
}
}


Viewing Delivery Logs

You can see all webhook deliveries and debug any failures from within the app.

Click View logs from the same webhook endpoint menu to see delivery history.

Click View to see the full request payload and response.


Regenerating Your Secret

If your secret is compromised, you can regenerate it from the webhook settings.

⚠️ Note: Your old secret stops working immediately. Update your server with the new secret right away.


Best Practices

  1. Always verify signatures - Never process unverified webhooks
  2. Respond quickly - Return a 2xx response within 10 seconds
  3. Process async - Queue heavy processing, respond immediately
  4. Handle duplicates - Use X-Contentpen-Delivery header to deduplicate
  5. Store secrets securely - Use environment variables, not hardcoded values


Need Help?

If you're having trouble setting up webhooks, check the delivery logs first to see what's happening. If you're still stuck, reach out to our support team.

Updated on: 30/12/2025

Was this article helpful?

Share your feedback

Cancel

Thank you!