Contentpen
Dark mode
Webhooks

Webhooks

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


Was this article helpful?