Upgrid
Integrations

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:

  1. Upgrid converts the article content to both HTML and Markdown formats
  2. A JSON payload is created with article data (title, content, slug, images, etc.)
  3. The payload is sent via HTTP POST to your webhook URL
  4. Your endpoint receives the data and processes it (save to database, publish to CMS, etc.)
  5. Your endpoint returns a 200 status code to confirm receipt

Setting Up Your Webhook

Step 1: Configure Your Webhook Endpoint

  1. Go to Settings → Integrations in your Upgrid dashboard
  2. Click Connect Webhook
  3. Enter your webhook URL (e.g., https://your-domain.com/api/webhook)
  4. 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:

  1. Upgrid generates a unique secret key for your webhook
  2. Copy this key immediately - you'll need it to verify incoming requests
  3. Store the key securely (e.g., as UPGRID_WEBHOOK_SECRET environment 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

  1. Click Send Test to send a test payload to your endpoint
  2. Verify your endpoint receives the data and returns status 200
  3. 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

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-Signature header
  • 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

FieldTypeDescription
titlestringArticle title
content_htmlstringArticle content rendered as HTML
content_markdownstringArticle content in Markdown format
slugstringURL-friendly slug for the article
meta_descriptionstringSEO meta description (optional)
statusstringAlways "published" for webhooks
featured_imagestringURL to cover image (optional)
published_urlstringURL where article was/will be published (optional)
scheduled_datestringISO 8601 timestamp of scheduled publication (optional)
published_atstringISO 8601 timestamp of actual publication
is_republishbooleantrue if updating existing article, false for new article
testbooleantrue 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

  1. Create a new Zap
  2. Choose Webhooks by Zapier as the trigger
  3. Select Catch Hook
  4. Copy the provided webhook URL
  5. In Upgrid, paste this URL and select Bearer Token authentication
  6. Copy your Upgrid secret key to Zapier's authentication field
  7. Send a test from Upgrid to populate the sample data
  8. Connect your desired action (Google Sheets, Airtable, etc.)

Make.com Integration

  1. Create a new scenario
  2. Add a Webhook module
  3. Create a new webhook and copy the URL
  4. In Upgrid, paste this URL and select Bearer Token authentication
  5. In Make.com, add header validation:
    • Header name: Authorization
    • Expected value: Bearer YOUR_SECRET_KEY
  6. Send a test from Upgrid
  7. 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: true and 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 slug field
  • Check the is_republish field to handle updates differently
  • Ensure your unique constraint is on the slug field, not just id

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-signature or 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:

  1. Go to your webhook integration settings
  2. View Last Test timestamp and status
  3. Check Connection Status indicator
  4. 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 Upgrid

For additional support, contact our team through the in-app support chat or check our API documentation for advanced webhook customization.

We use cookies

We use cookies to ensure you get the best experience on our website. By clicking "Accept", you agree to our use of cookies.