Jenna.so Logo

Publishing with Webhooks Guide

Overview

This guide explains how to configure and use webhooks to receive notifications and data about events happening in your project, such as when a blog post has been generated and is ready for publishing. By setting up a webhook endpoint, you can integrate our platform directly with your own systems or workflows.

This guide covers:

  • Enabling Webhook Integration
  • Managing Webhook Endpoints
  • Understanding Webhook Events and Payloads
  • Verifying Request Signatures
  • Handling Delivery Attempts and Retries

Enabling Webhook Integration

To start receiving webhooks, you first need to enable the Webhook publishing integration for your project:

  1. Navigate to your Project Settings.
  2. Find the Publishing Integration section.
  3. Locate the Webhook option.
  4. Toggle the switch to enable it.

Note: Only one publishing integration method can be active at a time. Enabling webhooks will disable any other active integration (e.g., WordPress, Wix).

Managing Webhook Endpoints

Once the Webhook integration is enabled, you can manage the specific endpoints where notifications will be sent.

  1. In Project Settings, find the Webhook Endpoints section (this appears after enabling the Webhook integration).
  2. Add an Endpoint: Click "Add Endpoint". You will need to provide:
    • Webhook URL: The HTTPS URL of your server endpoint that will receive the webhook POST requests.
    • Webhook Signing Secret: A secret key known only to you and our system. This is used to generate a signature for verifying the request's authenticity. Keep this secret secure.
  3. Edit an Endpoint: Click on an existing endpoint in the list to modify its URL or Signing Secret.
  4. Delete an Endpoint: Use the delete icon next to an endpoint to remove it.

You can configure multiple endpoints if needed, although typically one is sufficient.

Webhook Events

Our system sends webhooks for specific events. Currently, the primary event is:

  • BLOG_POST_GENERATED: Sent when a blog post has finished the generation process and is ready for review or publishing.

Payload Structure

All webhook requests are sent as HTTP POST requests with a JSON body (Content-Type: application/json). The payload follows this general structure:

{
  "metadata": {
    "event_type": "BLOG_POST_GENERATED", // The type of event
    "timestamp": "2023-10-27T10:00:00.123Z", // ISO 8601 timestamp of when the event occurred
    "webhook_id": "...", // Unique ID for this specific delivery attempt
  },
  "data": {
    // Event-specific data structure for BLOG_POST_GENERATED
    "blog_post": {
        "id": "clxka123b0001abcd1efghijk",
        "headline": "Your Awesome Blog Post Headline",
        "slug": "your-awesome-blog-post-headline",
        "targetedKeywords": ["targeted keyword 1", "targeted keyword 2"],
        "mainImageUrl": "https://your-image-host.com/image.jpg", 
        "createdAt": "2023-10-27T09:58:00.000Z", 
        "updatedAt": "2023-10-27T10:00:00.000Z",
        "projectId": "clxka456d0002bcde2fghijlk",
        "projectName": "My Awesome Project", 
        "projectWebsiteUrl": "https://my-awesome-project.com",
        "markdownContent": "# Your Awesome Blog Post Headline\n\nThis is the introduction...\n\n## Section 1\n\nContent for section 1...\n\n### Sub-section 1.1\n\nMore details...\n\n## Conclusion\n\nFinal thoughts...",
        "htmlContent": "<h1 style=\"font-size: 2rem; font-weight: bold; margin: 2rem 0 1rem 0;\">Your Awesome Blog Post Headline</h1><div style=\"margin: 0 0 1rem 0; line-height: 1.6;\">This is the introduction...</div><h2 style=\"font-size: 1.5rem; font-weight: bold; margin: 1.5rem 0 0.75rem 0;\">Section 1</h2><div style=\"margin: 0 0 1rem 0;\">Content for section 1...</div><!-- CTA section with inline CSS --><div style=\"background-color: #f8fafc; border-radius: 12px; padding: 2rem; text-align: center; margin: 2rem 0;\">...</div><h2 style=\"font-size: 1.5rem; font-weight: bold;\">Conclusion</h2><div style=\"margin: 0 0 1rem 0;\">Final thoughts...</div>",
        "metaDescription": "An awesome meta description for SEO.",
        "metaKeywords": ["keyword1", "keyword2", "keyword3"],
        "ctaData": {
            "headline": "Ready to Get Started?",
            "subheadline": "Join thousands of satisfied customers today",
            "buttonText": "Get Started Now",
            "buttonUrl": "https://my-awesome-project.com/signup",
            "styling": {
                "section": {
                    "backgroundColor": "#f8fafc",
                    "borderRadius": 12,
                    "shadow": "sm"
                },
                "headline": {
                    "fontSize": 24,
                    "fontWeight": "bold",
                    "underline": false,
                    "italic": false,
                    "color": "#1a1a1a",
                    "alignment": "center"
                },
                "subheadline": {
                    "fontSize": 16,
                    "fontWeight": "normal",
                    "underline": false,
                    "italic": false,
                    "color": "#666666",
                    "alignment": "center"
                },
                "button": {
                    "text": {
                        "fontSize": 16,
                        "fontWeight": "bold",
                        "underline": false,
                        "italic": false,
                        "color": "#ffffff"
                    },
                    "backgroundColor": "#3b82f6",
                    "borderRadius": 8,
                    "shadow": "md",
                    "alignment": "center"
                }
            }
        }
    }
  }
}

(Note: The exact fields within data.blog_post may evolve. Rely on the fields present in the actual payload.)

Request Headers

Each webhook POST request includes the following headers:

  • Content-Type: application/json
  • User-Agent: Jenna-Webhooks/1.0 (Identifies the request source)
  • X-Webhook-ID: The unique identifier for this specific webhook delivery attempt (same as metadata.webhook_id in the payload).
  • X-Signature: A SHA-256 HMAC signature of the raw request body, generated using your Webhook Signing Secret.

Signature Verification

To ensure the security and authenticity of webhooks, you must verify the X-Signature header. This confirms the request originated from our system and wasn't tampered with.

Verification Steps:

  1. Get the raw request body: Access the unmodified, raw request body as a string (before any JSON parsing). Many web frameworks require specific middleware to access the raw body.
  2. Get the received signature: Extract the value from the X-Signature header.
  3. Retrieve your secret: Get the Webhook Signing Secret you configured for the endpoint.
  4. Generate the expected signature: Compute an HMAC with the SHA256 hash function. Use your Webhook Signing Secret as the key and the raw request body (as a UTF-8 string) as the message. Convert the resulting HMAC digest to a hexadecimal string.
  5. Compare signatures: Use a constant-time comparison function to compare the signature received in the X-Signature header with the signature you generated. If they match, the webhook is valid.

Example Implementation (Next.js API Route)

const crypto = require('crypto')

export const POST = async (req : Request) => {
    // validate signature
    const rawBody = await new Response(req.body).text();
    const secret = process.env.JENNA_SIGNING_SECRET
    const hmac = crypto.createHmac('sha256', secret)
    const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8');
    const signature = Buffer.from(req.headers.get('X-Signature') || '', 'utf8');

    if (!crypto.timingSafeEqual(digest, signature)) {
        throw new Error('Invalid signature');
    }

    // process the webhook
    //...

    return new Response(null, { status: 200 })
}

Delivery Attempts and Retries

If your endpoint fails to respond with a 2xx status code (e.g., 200 OK) within 30 seconds, or if there's a network error preventing delivery, our system will automatically retry sending the webhook.

  • Retry Schedule: Retries occur with an exponential backoff strategy (e.g., 1 minute, 5 minutes, 15 minutes, etc.). A small amount of random jitter is added to the delay.
  • Maximum Retries: The system will attempt delivery multiple times (currently up to 5 retries) before marking the delivery as permanently failed.
  • attempt Metadata: The metadata.attempt field in the payload indicates which delivery attempt it is (1 for the first, 2 for the second, etc.).

Your endpoint should be idempotent, meaning receiving the same webhook multiple times should not cause duplicate actions or errors. Verifying the X-Webhook-ID can help with deduplication if needed.

Best Practices

  1. Return 200 OK Quickly: Acknowledge receipt of the webhook by returning a 2xx status code as soon as possible (ideally within a few seconds). Perform complex processing asynchronously in the background. Timeouts will cause retries.
  2. Verify Signatures: Always verify the X-Signature before trusting the payload.
  3. Use HTTPS: Ensure your webhook endpoint URL uses HTTPS for secure data transmission.
  4. Store Secrets Securely: Never hardcode your Webhook Signing Secret. Use environment variables or a dedicated secret management system.
  5. Use Constant-Time Comparison: Prevent timing attacks by using functions like crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hash_equals (PHP).
  6. Handle Retries Gracefully (Idempotency): Design your endpoint to handle potential duplicate deliveries caused by retries.

Troubleshooting

Common issues and solutions:

  1. Invalid Signature:
    • Double-check your Webhook Signing Secret matches the one configured in our platform.
    • Ensure you are using the raw, unmodified request body for signature generation. Framework middleware often parses JSON automatically; you need the raw string/buffer before parsing.
    • Verify you are using the correct hash function (SHA256) and HMAC method.
    • Check for character encoding issues (should be UTF-8).
  2. Endpoint Timeouts:
    • Your endpoint is taking too long to respond (>30 seconds). Move processing to a background job and respond immediately with 200 OK.

Support

If you encounter persistent issues implementing webhook integration:

  1. Review this documentation carefully, especially the Best Practices and Troubleshooting sections.
  2. Compare your implementation against the example code.
  3. Contact our support team with specific details: the X-Webhook-ID of a failed delivery, error messages from your logs, and relevant code snippets.

Grow Organic SEO Traffic, Today

Stop wrestling with content creation. Your dedicated AI writer generates SEO-optimized blog posts that drive traffic, so you can focus on building your business.

Get More SEO Traffic