Webhook Integration
Send article data to any URL for custom integrations with your CMS, automation platform, or serverless function
How to Integrate Upgrid with Webhooks
Webhooks allow Upgrid to send real-time article data to any URL endpoint when an article is published. This powerful integration method enables you to connect Upgrid to custom CMS platforms, automation tools, serverless functions, or any service that accepts HTTP POST requests.
Common Use Cases
Webhooks are perfect for:
- Custom CMS Integration - Next.js, Hugo, Jekyll, or custom-built platforms
- Automation Platforms - Zapier, Make.com, n8n, or similar tools
- Serverless Functions - AWS Lambda, Vercel Functions, Netlify Functions
- Notification Systems - Slack, Discord, email notifications
- Custom Workflows - Any service accepting HTTP POST requests
Requirements
Before you begin, you need:
- A publicly accessible URL endpoint that accepts POST requests
- Ability to add server-side code to verify webhook signatures (recommended for security)
- HTTPS endpoint (recommended for production use)
How Webhooks Work
When an article is published in Upgrid:
- Upgrid converts the article content to both HTML and Markdown formats
- A JSON payload is created with article data (title, content, slug, images, etc.)
- The payload is sent via HTTP POST to your webhook URL
- Your endpoint receives the data and processes it (save to database, publish to CMS, etc.)
- Your endpoint returns a 200 status code to confirm receipt
Setting Up Your Webhook
Step 1: Configure Your Webhook Endpoint
- Go to Settings → Integrations in your Upgrid dashboard
- Click Connect Webhook
- Enter your webhook URL (e.g.,
https://your-domain.com/api/webhook) - Choose your authentication type:
- Signature-based (HMAC SHA-256) - More secure, recommended for production
- Bearer Token - Simpler, ideal for no-code platforms like Zapier
Step 2: Save and Copy Your Secret Key
After saving your webhook configuration:
- Upgrid generates a unique secret key for your webhook
- Copy this key immediately - you'll need it to verify incoming requests
- Store the key securely (e.g., as
UPGRID_WEBHOOK_SECRETenvironment variable)
⚠️ Important: Keep your secret key secure. Anyone with this key can send requests that appear to be from Upgrid.
Step 3: Test Your Connection
- Click Send Test to send a test payload to your endpoint
- Verify your endpoint receives the data and returns status 200
- Check the test result in Upgrid to confirm connection status
Once connected and tested, your webhook is ready to receive article data automatically.
Authentication Methods
Option 1: Signature-based Authentication (Recommended)
Signature-based authentication uses HMAC SHA-256 to cryptographically verify that requests come from Upgrid. This is the most secure option.
How it works:
- Upgrid creates a signature of the request payload using your secret key
- The signature is sent in the
X-Upgrid-Signatureheader - Your endpoint recalculates the signature and compares it to verify authenticity
Implementation example (Next.js):
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
export async function POST(req: NextRequest) {
try {
// Get the raw body
const body = await req.text()
// Get signature from header
const signature = req.headers.get('X-Upgrid-Signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
)
}
// Verify signature
const secret = process.env.UPGRID_WEBHOOK_SECRET!
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
if (signature !== expectedSignature) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
// Parse the verified payload
const payload = JSON.parse(body)
// Process the article data
await processArticle(payload)
return NextResponse.json({ success: true })
} catch (error) {
console.error('Webhook error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
async function processArticle(payload: any) {
// Skip test payloads if you don't want to save them
if (payload.test) {
console.log('Received test webhook')
return
}
// Your logic to save/publish the article
// For example, save to database, publish to CMS, etc.
}Option 2: Bearer Token Authentication
Bearer token authentication is simpler and works great for no-code platforms like Zapier or Make.com.
How it works:
- Upgrid sends your secret key in the
Authorization: Bearer <token>header - Your endpoint verifies the token matches your stored secret
Implementation example:
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
try {
// Get authorization header
const authHeader = req.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Missing or invalid authorization' },
{ status: 401 }
)
}
const token = authHeader.substring(7) // Remove 'Bearer ' prefix
const expectedToken = process.env.UPGRID_WEBHOOK_SECRET!
if (token !== expectedToken) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
// Parse payload
const payload = await req.json()
// Process the article data
await processArticle(payload)
return NextResponse.json({ success: true })
} catch (error) {
console.error('Webhook error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Webhook Payload Structure
Upgrid sends a JSON payload with the following structure:
{
"title": "Your Article Title",
"content_html": "<h1>Your Article Title</h1><p>Article content...</p>",
"content_markdown": "# Your Article Title\n\nArticle content...",
"slug": "your-article-title",
"meta_description": "Brief description for SEO",
"status": "published",
"featured_image": "https://cdn.upgrid.com/images/abc123.jpg",
"published_url": "https://your-site.com/blog/your-article-title",
"scheduled_date": "2025-01-15T10:00:00.000Z",
"published_at": "2025-01-15T10:00:00.000Z",
"is_republish": false,
"test": false
}Payload Fields
| Field | Type | Description |
|---|---|---|
title | string | Article title |
content_html | string | Article content rendered as HTML |
content_markdown | string | Article content in Markdown format |
slug | string | URL-friendly slug for the article |
meta_description | string | SEO meta description (optional) |
status | string | Always "published" for webhooks |
featured_image | string | URL to cover image (optional) |
published_url | string | URL where article was/will be published (optional) |
scheduled_date | string | ISO 8601 timestamp of scheduled publication (optional) |
published_at | string | ISO 8601 timestamp of actual publication |
is_republish | boolean | true if updating existing article, false for new article |
test | boolean | true for test payloads, false for real articles |
Complete Implementation Examples
Next.js with Database Upsert
This example shows how to handle both new articles and updates using an upsert pattern:
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
import { db } from '@/lib/db'
export async function POST(req: NextRequest) {
try {
// Verify signature (signature-based auth)
const body = await req.text()
const signature = req.headers.get('X-Upgrid-Signature')
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 })
}
const expectedSignature = crypto
.createHmac('sha256', process.env.UPGRID_WEBHOOK_SECRET!)
.update(body)
.digest('hex')
if (signature !== expectedSignature) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
// Parse verified payload
const payload = JSON.parse(body)
// Skip test webhooks
if (payload.test) {
console.log('Test webhook received successfully')
return NextResponse.json({ success: true, message: 'Test webhook received' })
}
// Upsert article to database
const article = await db.article.upsert({
where: { slug: payload.slug },
update: {
title: payload.title,
contentHtml: payload.content_html,
contentMarkdown: payload.content_markdown,
metaDescription: payload.meta_description,
featuredImage: payload.featured_image,
updatedAt: new Date(),
},
create: {
slug: payload.slug,
title: payload.title,
contentHtml: payload.content_html,
contentMarkdown: payload.content_markdown,
metaDescription: payload.meta_description,
featuredImage: payload.featured_image,
status: 'published',
publishedAt: new Date(payload.published_at),
},
})
console.log(
payload.is_republish
? `Updated article: ${article.slug}`
: `Created article: ${article.slug}`
)
return NextResponse.json({
success: true,
articleId: article.id,
action: payload.is_republish ? 'updated' : 'created',
})
} catch (error) {
console.error('Webhook processing error:', error)
return NextResponse.json(
{ error: 'Failed to process webhook' },
{ status: 500 }
)
}
}Vercel Serverless Function
// api/webhook.ts
import type { VercelRequest, VercelResponse } from '@vercel/node'
import crypto from 'crypto'
export default async function handler(
req: VercelRequest,
res: VercelResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
// Get raw body as string
const body = JSON.stringify(req.body)
const signature = req.headers['x-upgrid-signature'] as string
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.UPGRID_WEBHOOK_SECRET!)
.update(body)
.digest('hex')
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' })
}
const payload = req.body
// Process article
if (!payload.test) {
// Your logic here
console.log('Processing article:', payload.title)
}
return res.status(200).json({ success: true })
} catch (error) {
console.error('Webhook error:', error)
return res.status(500).json({ error: 'Internal server error' })
}
}Express.js Endpoint
// server.js
const express = require('express')
const crypto = require('crypto')
const app = express()
// Important: Use raw body for signature verification
app.use('/api/webhook', express.raw({ type: 'application/json' }))
app.post('/api/webhook', async (req, res) => {
try {
// Get signature
const signature = req.headers['x-upgrid-signature']
if (!signature) {
return res.status(401).json({ error: 'Missing signature' })
}
// Verify signature using raw body
const expectedSignature = crypto
.createHmac('sha256', process.env.UPGRID_WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Parse the verified payload
const payload = JSON.parse(req.body.toString())
// Skip test webhooks
if (payload.test) {
console.log('Test webhook received')
return res.json({ success: true })
}
// Process article
await processArticle(payload)
res.json({ success: true })
} catch (error) {
console.error('Webhook error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})
async function processArticle(payload) {
// Your article processing logic
console.log('Processing article:', payload.title)
}
app.listen(3000, () => console.log('Server running on port 3000'))Handling Updates vs New Articles
Use the is_republish field to determine if you should create a new article or update an existing one:
async function handleArticle(payload: any) {
if (payload.is_republish) {
// Update existing article
await updateArticle({
slug: payload.slug,
title: payload.title,
content: payload.content_html,
// ... other fields
})
} else {
// Create new article
await createArticle({
slug: payload.slug,
title: payload.title,
content: payload.content_html,
// ... other fields
})
}
}Best Practice: Use an upsert operation (update or insert) based on the slug field. This handles both cases gracefully and prevents duplicates:
// Prisma example
await prisma.article.upsert({
where: { slug: payload.slug },
update: { /* updated fields */ },
create: { /* all fields */ },
})
// MongoDB example
await Article.findOneAndUpdate(
{ slug: payload.slug },
{ $set: { /* updated fields */ } },
{ upsert: true, new: true }
)Integration with Zapier / Make.com
Zapier Integration
- Create a new Zap
- Choose Webhooks by Zapier as the trigger
- Select Catch Hook
- Copy the provided webhook URL
- In Upgrid, paste this URL and select Bearer Token authentication
- Copy your Upgrid secret key to Zapier's authentication field
- Send a test from Upgrid to populate the sample data
- Connect your desired action (Google Sheets, Airtable, etc.)
Make.com Integration
- Create a new scenario
- Add a Webhook module
- Create a new webhook and copy the URL
- In Upgrid, paste this URL and select Bearer Token authentication
- In Make.com, add header validation:
- Header name:
Authorization - Expected value:
Bearer YOUR_SECRET_KEY
- Header name:
- Send a test from Upgrid
- Connect your desired modules
Best Practices
Security
- Always verify signatures or tokens - Never trust incoming requests without authentication
- Use HTTPS endpoints - Unencrypted HTTP exposes your secret key
- Store secrets securely - Use environment variables, never hardcode secrets
- Implement rate limiting - Protect against abuse
- Return 200 quickly - Process data asynchronously if needed
Reliability
- Return 200 status code - Upgrid considers non-200 responses as failures
- Handle test webhooks - Check for
test: trueand skip database operations - Use upsert operations - Prevent duplicate articles when handling updates
- Implement retry logic - For external API calls in your processing
- Log errors properly - Help with debugging when issues occur
Performance
- Respond quickly - Don't make your webhook wait for long operations
- Process asynchronously - Queue heavy tasks (image processing, external APIs)
- Validate payload early - Check required fields before processing
- Use database transactions - Ensure data consistency
Troubleshooting
Connection Test Fails
Problem: Test webhook returns error or timeout
Solutions:
- Verify your endpoint is publicly accessible (not localhost)
- Check your server logs for errors
- Ensure endpoint returns 200 status code
- Verify HTTPS certificate is valid (if using HTTPS)
- Check firewall rules aren't blocking Upgrid's requests
Authentication Errors
Problem: "Invalid signature" or "Invalid token" errors
Solutions:
- Verify you copied the entire secret key correctly
- Check for extra spaces or line breaks in your environment variable
- For signature auth: ensure you're hashing the raw request body
- For bearer auth: verify Authorization header format is
Bearer <token> - Don't modify the request body before verification
Duplicate Articles Created
Problem: Updates create new articles instead of updating existing ones
Solutions:
- Use upsert operations based on the
slugfield - Check the
is_republishfield to handle updates differently - Ensure your unique constraint is on the
slugfield, not justid
Missing Webhook Headers
Problem: Cannot find X-Upgrid-Signature or Authorization header
Solutions:
- Some frameworks normalize header names to lowercase
- Try accessing as
x-upgrid-signatureor use case-insensitive lookup - Check if your framework or proxy is stripping headers
- Verify your server logs show the headers in incoming requests
Test Webhooks Saved to Database
Problem: Test payloads are being saved as real articles
Solutions:
// Always check for test flag
if (payload.test) {
console.log('Test webhook - skipping database save')
return NextResponse.json({ success: true })
}
// Only process real articles
await saveArticle(payload)Monitoring and Debugging
Check Webhook Logs
In Upgrid dashboard:
- Go to your webhook integration settings
- View Last Test timestamp and status
- Check Connection Status indicator
- Review error messages if connection fails
Server-side Logging
Add comprehensive logging to your endpoint:
export async function POST(req: NextRequest) {
console.log('[Webhook] Received request')
console.log('[Webhook] Headers:', Object.fromEntries(req.headers))
try {
const body = await req.text()
console.log('[Webhook] Body length:', body.length)
// ... verification logic
const payload = JSON.parse(body)
console.log('[Webhook] Payload:', {
title: payload.title,
slug: payload.slug,
is_republish: payload.is_republish,
test: payload.test,
})
// ... processing logic
console.log('[Webhook] Successfully processed')
return NextResponse.json({ success: true })
} catch (error) {
console.error('[Webhook] Error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}Frequently Asked Questions
Q: Can I use webhooks with localhost for development? A: No, webhook URLs must be publicly accessible. Use tools like ngrok, localtunnel, or Cloudflare Tunnel to expose your local server during development.
Q: What happens if my endpoint is down when an article is published? A: Upgrid will mark the publication as failed. You can retry publishing from the article management page in your dashboard.
Q: Can I have multiple webhook URLs? A: Currently, each site can have one webhook URL. If you need to send data to multiple services, implement fan-out logic in your webhook endpoint.
Q: How do I migrate from Bearer Token to Signature authentication? A: Simply update your webhook settings in Upgrid to use Signature authentication. A new secret key will be generated. Update your endpoint code to verify signatures instead of bearer tokens.
Q: Is there a rate limit on webhooks? A: Upgrid doesn't impose webhook-specific rate limits, but respect general API rate limits if you're calling back to Upgrid from your endpoint.
Q: Can I validate the webhook payload structure? A: Yes, implement schema validation using libraries like Zod:
import { z } from 'zod'
const webhookSchema = z.object({
title: z.string(),
content_html: z.string(),
content_markdown: z.string(),
slug: z.string(),
meta_description: z.string().optional(),
status: z.literal('published'),
featured_image: z.string().url().optional(),
published_at: z.string(),
is_republish: z.boolean(),
test: z.boolean().optional(),
})
// In your handler
const payload = webhookSchema.parse(JSON.parse(body))Q: How can I test my webhook locally? A: Use ngrok to create a public URL for your local server:
# Start your local server on port 3000
npm run dev
# In another terminal, start ngrok
ngrok http 3000
# Use the provided ngrok URL (e.g., https://abc123.ngrok.io/api/webhook) in UpgridFor additional support, contact our team through the in-app support chat or check our API documentation for advanced webhook customization.