Skip to content

On-Demand ISR — Triggering Revalidation via API Routes

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about On. We cover key concepts, practical examples, and best practices to help you master this topic.

On-demand ISR triggers page revalidation instantly via API routes, enabling immediate content updates when CMS publishes or edits content.

What You'll Learn

By the end of this tutorial, you'll understand how on-demand ISR works, how to create revalidation API endpoints, how to secure them with secrets, and how to integrate with CMS Webhooks.

Why It Matters

Time-based revalidation has a delay window. If a content editor publishes an article, it won't appear until the revalidate window expires. On-demand ISR eliminates this delay by triggering revalidation the moment content changes.

Real-World Use

A CMS editor publishes a new blog post. The CMS sends a Webhook to the revalidation API. The new post page and the blog listing page regenerate immediately. Readers see the new content within seconds of publishing.

On-Demand ISR Flow

graph TD
    A[Content Editor
publishes in CMS] --> B[CMS Webhook
POST to /api/revalidate] B --> C[Verify
webhook secret] C --> D{Valid secret?} D -->|No| E[Return 401] D -->|Yes| F[res.revalidate
specific paths] F --> G[Revalidate
post page] F --> H[Revalidate
listing pages] G --> I[New static HTML
generated] H --> I I --> J[CDN cache updated] J --> K[Users see
fresh content] style A fill:#27ae60,color:#fff style B fill:#4a90d9,color:#fff style F fill:#e67e22,color:#fff style J fill:#27ae60,color:#fff

Revalidation API Endpoint

// pages/api/revalidate.js — On-demand revalidation
export default async function handler(req, res) {
    // Only allow POST requests
    if (req.method !== 'POST') {
        return res.status(405).json({ message: 'Method not allowed' });
    }

    // Verify webhook secret
    const { secret, paths } = req.body;

    if (secret !== process.env.REVALIDATION_TOKEN) {
        return res.status(401).json({ message: 'Invalid secret' });
    }

    if (!paths || !Array.isArray(paths) || paths.length === 0) {
        return res.status(400).json({ message: 'Paths array required' });
    }

    try {
        // Revalidate all specified paths in parallel
        const results = await Promise.allSettled(
            paths.map(path => res.revalidate(path))
        );

        const succeeded = results.filter(r => r.status === 'fulfilled').length;
        const failed = results.filter(r => r.status === 'rejected');

        if (failed.length > 0) {
            console.error('Revalidation failures:', failed.map(f => f.reason));
        }

        return res.json({
            revalidated: true,
            succeeded,
            failed: failed.length,
            errors: failed.map(f => f.reason.message)
        });
    } catch (err) {
        console.error('Revalidation error:', err);
        return res.status(500).json({
            message: 'Error revalidating',
            error: err.message
        });
    }
}

CMS Webhook Integration

// Contentful webhook handler
export async function handleContentfulWebhook(secret) {
    return async function handler(req, res) {
        if (req.query.secret !== secret) {
            return res.status(401).json({ message: 'Invalid secret' });
        }

        const { fields, sys } = req.body;
        const contentType = sys.contentType.sys.id;
        const slug = fields?.slug?.['en-US'];

        const pathsToRevalidate = [];

        if (slug) {
            pathsToRevalidate.push(`/blog/${slug}`);
        }

        // Always revalidate listing pages
        pathsToRevalidate.push('/blog');
        pathsToRevalidate.push('/');

        // Revalidate category pages if applicable
        if (fields?.category) {
            const category = fields.category['en-US'];
            pathsToRevalidate.push(`/category/${category}`);
        }

        try {
            await Promise.all(
                pathsToRevalidate.map(path => res.revalidate(path))
            );
            res.json({ revalidated: true, paths: pathsToRevalidate });
        } catch (err) {
            res.status(500).json({ error: err.message });
        }
    };
}

// Sanity webhook handler
export async function handleSanityWebhook(req, res) {
    const signature = req.headers['sanity-signature'];
    const isValid = verifySanitySignature(signature, req.body);

    if (!isValid) {
        return res.status(401).json({ message: 'Invalid signature' });
    }

    const { slug, _type } = req.body;

    const paths = ['/'];
    if (_type === 'post') {
        paths.push(`/blog/${slug}`);
        paths.push('/blog');
    } else if (_type === 'product') {
        paths.push(`/products/${slug}`);
        paths.push('/products');
    }

    await Promise.all(paths.map(path => res.revalidate(path)));

    return res.json({ revalidated: true });
}

Serverless Webhook Handler

// pages/api/webhook.js — Generic webhook for multiple CMS
export default async function handler(req, res) {
    const { secret, source } = req.query;

    if (secret !== process.env.REVALIDATION_TOKEN) {
        return res.status(401).json({ message: 'Invalid token' });
    }

    // Parse webhook payload based on source
    let paths = [];
    const payload = req.body;

    switch (source) {
        case 'contentful':
            paths = parseContentfulPayload(payload);
            break;
        case 'sanity':
            paths = parseSanityPayload(payload);
            break;
        case 'strapi':
            paths = parseStrapiPayload(payload);
            break;
        case 'strapi':
            paths = parseStrapiPayload(payload);
            break;
        case 'webhook':
            paths = parseCustomWebhook(payload);
            break;
        default:
            return res.status(400).json({ message: 'Unknown source' });
    }

    // Include listing pages
    paths.push('/');

    try {
        await Promise.all(paths.map(path => res.revalidate(path)));
        res.json({
            revalidated: true,
            paths,
            timestamp: Date.now()
        });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
}

function parseContentfulPayload(body) {
    const paths = [];
    const slug = body?.fields?.slug?.['en-US'];
    if (slug) paths.push(`/blog/${slug}`);
    paths.push('/blog');
    return paths;
}

function parseSanityPayload(body) {
    const paths = [];
    const slug = body?.slug?.current;
    if (slug && body?._type === 'post') paths.push(`/blog/${slug}`);
    paths.push('/blog');
    return paths;
}

function parseStrapiPayload(body) {
    const paths = [];
    const slug = body?.entry?.slug;
    if (slug) paths.push(`/blog/${slug}`);
    paths.push('/blog');
    return paths;
}

Multiple Secret Management

// pages/api/revalidate.js — Multi-tenant revalidation
const SITES = {
    blog: process.env.BLOG_REVALIDATION_TOKEN,
    docs: process.env.DOCS_REVALIDATION_TOKEN,
    shop: process.env.SHOP_REVALIDATION_TOKEN
};

export default async function handler(req, res) {
    const { secret, site, paths } = req.body;

    // Verify secret for the specific site
    if (secret !== SITES[site]) {
        return res.status(401).json({ message: 'Invalid secret' });
    }

    try {
        await Promise.all(paths.map(path => res.revalidate(path)));
        res.json({
            revalidated: true,
            site,
            paths,
            time: Date.now()
        });
    } catch (err) {
        console.error(`Revalidation failed for ${site}:`, err);
        res.status(500).json({ error: err.message });
    }
}

Common Mistakes

  1. Exposing the revalidation endpoint without authentication. Anyone who finds your webhook URL can trigger rebuilds. Use a strong random secret token.
  2. Not validating the webhook payload. Always verify the webhook signature or secret. Unauthorized requests can trigger expensive rebuilds.
  3. Revalidating every path on every change. If a single page changes, only revalidate that page and its listing. Don't revalidate the entire site.
  4. Not handling concurrent revalidation requests. Multiple webhooks arriving simultaneously can overload the server. Queue or deduplicate requests.
  5. Forgetting to revalidate listing pages. When a new post is published, both the post page and the blog listing need revalidation.

Practice Questions

  1. How does on-demand ISR differ from time-based revalidation?
  2. How do you secure a revalidation API endpoint?
  3. What paths should you revalidate when a new blog post is published?
  4. How do you integrate on-demand ISR with a headless CMS webhook?
  5. How do you handle revalidation of multiple pages at once?

Challenge: Set up a complete on-demand ISR pipeline: create a revalidation API endpoint with secret authentication, configure a Contentful webhook to POST to your endpoint, verify that content updates appear within seconds, and add error logging.

FAQ

Can on-demand ISR revalidate the entire site?

Technically yes, but it's not recommended. Revalidate only the changed pages. Use a CI/CD rebuild for mass content updates.

What is the limit on on-demand revalidations?

Vercel allows 1 million revalidations per month on the Pro plan. Each revalidation counts against serverless function execution limits.

Do revalidations fail during high traffic?

They can if serverless functions are saturated. Implement retry logic and queue revalidation requests if failures occur.

How do I test on-demand ISR locally?

Run next build + next start locally. Send a POST request to localhost:3000/api/revalidate with the secret and paths. Verify the page regenerates.

Can I revalidate pages in another project?

No. res.revalidate only works for pages within the same Next.js application. Cross-project revalidation requires separate webhook calls.

Mini Project

Create a CMS-integrated blog with on-demand ISR: set up a revalidation API endpoint, configure a webhook simulator that sends mock CMS payloads, implement path-based revalidation for posts and listings, add error monitoring, and verify content updates appear within 2 seconds.

What's Next

You've mastered on-demand revalidation. Now explore the Stale-While-Revalidate Pattern in depth to understand how ISR serves content during revalidation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro