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-hereImportant: 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-hereWebhook 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:
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.
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
Error: 401 Unauthorized - "Invalid access token"
Solution: Verify that:
- The
NEXT_PUBLIC_WEBHOOK_ACCESS_TOKENenvironment variable is set - The token in the Authorization header matches your environment variable
- The header format is correct:
Bearer your-token
Error: 400 Bad Request - "Invalid payload structure"
Solution: Ensure the payload includes:
event_typefielddataobjectdata.articlesas an array
Error: Individual article processing fails
Solution: Verify each article has:
id- Unique identifiertitle- Article titleslug- 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_htmlif 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.