Webhook Integration Guide

This guide will help you set up a webhook endpoint in your Next.js application to receive articles from IntentRank services. You'll learn how to configure authentication, handle incoming payloads, and implement error handling.

Getting Started

Webhooks allow IntentRank to automatically send articles to your application when they're ready. This eliminates the need for polling and ensures your content is always up to date.

To get started, you'll need to:

  • Create a webhook endpoint in your Next.js application
  • Configure authentication using a Bearer token
  • Handle the incoming article payload
  • Store or process the articles as needed

Authentication

All webhook requests from IntentRank are authenticated using a Bearer token in the Authorization header. This ensures that only authorized requests can send articles to your endpoint.

Setting Up Your Access Token

First, generate a secure access token and store it as an environment variable in your Next.js application:

# .env.local
NEXT_PUBLIC_WEBHOOK_ACCESS_TOKEN=your-secure-token-here

Important: Use a strong, randomly generated token. You can generate one using:

# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Validating the Token

Your webhook endpoint should validate the Bearer token on every request. Here's how the Authorization header is structured:

Authorization: Bearer your-secure-token-here

Webhook Endpoint

In Next.js 13+ (App Router), create your webhook endpoint by adding a route handler at app/api/webhook/route.ts.

Basic Endpoint Structure

Here's a minimal webhook endpoint implementation:

import { NextRequest, NextResponse } from "next/server";

const ACCESS_TOKEN = process.env.NEXT_PUBLIC_WEBHOOK_ACCESS_TOKEN || "";

function validateAccessToken(req: NextRequest): boolean {
  const authHeader = req.headers.get("authorization");
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return false;
  }
  const token = authHeader.split(" ")[1];
  return token === ACCESS_TOKEN;
}

export async function POST(req: NextRequest) {
  // Validate authentication
  if (!validateAccessToken(req)) {
    return NextResponse.json(
      { error: "Invalid access token" },
      { status: 401 }
    );
  }

  try {
    // Parse and process the payload
    const payload = await req.json();
    
    // Your processing logic here
    
    return NextResponse.json(
      { message: "Webhook processed successfully" },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { error: "Internal server error", message: error.message },
      { status: 500 }
    );
  }
}

// Handle unsupported methods
export async function GET() {
  return NextResponse.json(
    { error: "Method not allowed. Use POST." },
    { status: 405 }
  );
}

Payload Structure

IntentRank sends webhook payloads in JSON format with the following structure:

WebhookPayload Type
interface WebhookPayload {
  event_type: string;      // Type of event (e.g., "article.created")
  timestamp: string;        // ISO 8601 timestamp
  data: {
    articles: Article[];    // Array of article objects
  };
}

Payload Fields

  • event_type: A string identifying the type of event. Currently supports article-related events.
  • timestamp: ISO 8601 formatted timestamp indicating when the event occurred.
  • data.articles: An array of article objects. Can contain one or more articles in a single webhook call.

Article Structure

Each article in the payload follows a specific structure. Some fields are required, while others are optional.

Article Type
interface Article {
  id: string;                    // Required: Unique article identifier
  title: string;                 // Required: Article title
  slug: string;                   // Required: URL-friendly identifier
  content_html?: string;          // Optional: HTML content
  content_markdown?: string;      // Optional: Markdown content
  meta_description?: string;     // Optional: SEO meta description
  image_url?: string;             // Optional: Featured image URL
  tags?: string[];                // Optional: Array of tag strings
  created_at?: string;            // Optional: ISO 8601 creation date
}

Required Fields

The following fields are required and must be present in every article:

  • id - Unique identifier for the article
  • title - The article's title
  • slug - URL-friendly identifier (typically derived from the title)

Optional Fields

These fields enhance the article but are not required:

  • content_html: Full HTML content of the article. May include markdown code block markers that should be cleaned.
  • content_markdown: Markdown version of the content (if available).
  • meta_description: SEO meta description for the article.
  • image_url: URL to the featured image. May be a signed URL that expires.
  • tags: Array of relevant tags for categorization.
  • created_at: ISO 8601 formatted creation timestamp.

Request Example

Here's a complete example of a webhook request from IntentRank:

cURL Example

curl -X POST https://your-domain.com/api/webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-secure-token-here" \
  -d '{
    "event_type": "article.created",
    "timestamp": "2024-01-15T10:30:00Z",
    "data": {
      "articles": [
        {
          "id": "article-123",
          "title": "How to Optimize Your SEO Strategy",
          "slug": "how-to-optimize-seo-strategy",
          "content_html": "<h1>Introduction</h1><p>SEO optimization is crucial...</p>",
          "meta_description": "Learn how to optimize your SEO strategy with these proven techniques.",
          "image_url": "https://example.com/image.jpg",
          "tags": ["SEO", "Marketing", "Content"],
          "created_at": "2024-01-15T10:30:00Z"
        }
      ]
    }
  }'

Complete Next.js Implementation

Here's a complete, production-ready webhook handler:

import { NextRequest, NextResponse } from "next/server";
import type { WebhookPayload } from "@/types/article";

const ACCESS_TOKEN = process.env.NEXT_PUBLIC_WEBHOOK_ACCESS_TOKEN || "";

function validateAccessToken(req: NextRequest): boolean {
  const authHeader = req.headers.get("authorization");
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return false;
  }
  const token = authHeader.split(" ")[1];
  return token === ACCESS_TOKEN;
}

export async function POST(req: NextRequest) {
  // Validate authentication
  if (!validateAccessToken(req)) {
    return NextResponse.json(
      { error: "Invalid access token" },
      { status: 401 }
    );
  }

  try {
    // Parse request body
    const payload: WebhookPayload = await req.json();

    // Validate payload structure
    if (
      !payload.event_type ||
      !payload.data ||
      !Array.isArray(payload.data.articles)
    ) {
      return NextResponse.json(
        { error: "Invalid payload structure" },
        { status: 400 }
      );
    }

    // Process each article
    const results = await Promise.allSettled(
      payload.data.articles.map(async (article) => {
        // Validate required fields
        if (!article.id || !article.title || !article.slug) {
          throw new Error(
            `Invalid article: missing required fields`
          );
        }

        // Clean HTML content if needed (remove markdown code block markers)
        let cleanedContentHtml = article.content_html;
        if (cleanedContentHtml) {
          cleanedContentHtml = cleanedContentHtml.trim();
          const backtick = String.fromCharCode(96);
          const codeBlockHtml = backtick + backtick + backtick + 'html';
          const codeBlock = backtick + backtick + backtick;
          if (cleanedContentHtml.startsWith(codeBlockHtml)) {
            cleanedContentHtml = cleanedContentHtml.substring(7).trim();
          } else if (cleanedContentHtml.startsWith(codeBlock)) {
            cleanedContentHtml = cleanedContentHtml.substring(3).trim();
          }
          if (cleanedContentHtml.endsWith(codeBlock)) {
            cleanedContentHtml = cleanedContentHtml.substring(
              0,
              cleanedContentHtml.length - 3
            ).trim();
          }
        }

        // Process image if present (download and save to your storage)
        // ... your image processing logic

        // Save article to your database or storage
        // ... your save logic

        return {
          id: article.id,
          slug: article.slug,
          status: "saved",
        };
      })
    );

    // Check for failures
    const failures = results.filter((r) => r.status === "rejected");
    if (failures.length > 0) {
      const successCount = results.length - failures.length;
      if (successCount > 0) {
        // Partial success
        return NextResponse.json(
          {
            message: `Partially successful: ${successCount} articles saved, ${failures.length} failed`,
            saved: successCount,
            failed: failures.length,
            errors: failures.map((f) =>
              f.status === "rejected"
                ? f.reason?.message || "Unknown error"
                : null
            ),
          },
          { status: 207 } // Multi-Status
        );
      }

      // Complete failure
      return NextResponse.json(
        {
          error: "Failed to save articles",
          errors: failures.map((f) =>
            f.status === "rejected"
              ? f.reason?.message || "Unknown error"
              : null
          ),
        },
        { status: 500 }
      );
    }

    // All articles saved successfully
    return NextResponse.json(
      {
        message: "Webhook processed successfully",
        articles_saved: results.length,
        articles: results.map((r) =>
          r.status === "fulfilled" ? r.value : null
        ),
      },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      {
        error: "Internal server error",
        message: error.message || "An unexpected error occurred",
      },
      { status: 500 }
    );
  }
}

export async function GET() {
  return NextResponse.json(
    { error: "Method not allowed. Use POST." },
    { status: 405 }
  );
}

Response Format

Your webhook endpoint should return appropriate HTTP status codes and JSON responses. Here are the expected response formats:

Success Response (200)

Returned when all articles are processed successfully:

{
  "message": "Webhook processed successfully",
  "articles_saved": 2,
  "articles": [
    {
      "id": "article-123",
      "slug": "how-to-optimize-seo-strategy",
      "status": "saved"
    },
    {
      "id": "article-124",
      "slug": "advanced-seo-techniques",
      "status": "saved"
    }
  ]
}

Partial Success Response (207)

Returned when some articles succeed but others fail:

{
  "message": "Partially successful: 1 articles saved, 1 failed",
  "saved": 1,
  "failed": 1,
  "errors": [
    "Invalid article: missing required fields (title)"
  ]
}

Error Responses

Various error scenarios and their responses:

Authentication Error (401)

{
  "error": "Invalid access token"
}

Invalid Payload (400)

{
  "error": "Invalid payload structure"
}

Server Error (500)

{
  "error": "Internal server error",
  "message": "Failed to save articles"
}

Method Not Allowed (405)

{
  "error": "Method not allowed. Use POST."
}

Error Handling

Proper error handling ensures your webhook endpoint is robust and provides useful feedback to IntentRank services.

Common Errors and Solutions

Invalid Access Token

Error: 401 Unauthorized - "Invalid access token"

Solution: Verify that:

  • The NEXT_PUBLIC_WEBHOOK_ACCESS_TOKEN environment variable is set
  • The token in the Authorization header matches your environment variable
  • The header format is correct: Bearer your-token
Invalid Payload Structure

Error: 400 Bad Request - "Invalid payload structure"

Solution: Ensure the payload includes:

  • event_type field
  • data object
  • data.articles as an array
Missing Required Article Fields

Error: Individual article processing fails

Solution: Verify each article has:

  • id - Unique identifier
  • title - Article title
  • slug - URL-friendly identifier

Processing Multiple Articles

When processing multiple articles, use Promise.allSettled() to handle partial failures gracefully. This ensures that if one article fails, others can still be processed successfully.

Best Practices

Follow these best practices to ensure a reliable webhook integration:

Security

  • Use HTTPS: Always use HTTPS for your webhook endpoint to protect data in transit.
  • Validate Authentication: Always validate the Bearer token before processing any request.
  • Store Tokens Securely: Never commit access tokens to version control. Use environment variables or secure secret management.
  • Rate Limiting: Consider implementing rate limiting to prevent abuse.

Reliability

  • Idempotency: Design your endpoint to handle duplicate requests gracefully. Check if an article already exists before creating it.
  • Error Logging: Log all errors with sufficient context for debugging.
  • Retry Logic: IntentRank may retry failed requests. Ensure your endpoint can handle retries without creating duplicates.
  • Async Processing: For heavy processing, consider queuing articles for background processing and returning a 202 Accepted response immediately.

Content Processing

  • Clean HTML Content: Remove markdown code block markers (```html, ```) from content_html if present.
  • Image Handling: Download and store images from signed URLs to your own storage, as signed URLs may expire.
  • Content Validation: Validate and sanitize HTML content before storing to prevent XSS attacks.

Performance

  • Batch Processing: Process multiple articles in parallel when possible.
  • Database Optimization: Use bulk insert operations when saving multiple articles.
  • Response Time: Keep response times under 5 seconds. For longer operations, use async processing.

Next Steps

Now that you understand how to set up webhooks, you can:

  • Configure your webhook endpoint in your IntentRank dashboard
  • Test your endpoint using the provided examples
  • Monitor webhook deliveries and handle errors appropriately
  • Implement additional features like webhook signature verification

If you need help or have questions, please reach out to our support team or check our API reference documentation.