401 Unauthorized" when processing a Webhook

I’m receiving a 401 Unauthorized when I try to send a webhook to the https://13f1-...-859.ngrok-free.app/api/webhooks/clerk endpoint.

Issue

I’m receiving a 401 Unauthorized when sending a webhook to the provided endpoint.

Steps to Reproduce

  1. Set up the route handler (app/api/webhooks/clerk/route.ts):
    import { db } from "@/db/db";
    import { playlists, users } from "@/db/schema";
    import type { User } from "@clerk/nextjs/api";
    import { headers } from "next/headers";
    import { Webhook } from "svix";
    import { eq, isNull, inArray } from "drizzle-orm";
    
    type UnwantedKeys = "primaryEmailAddressId" | "primaryPhoneNumberId" | "phoneNumbers";
    
    interface UserInterface extends Omit<User, UnwantedKeys> {
        email_addresses: {
            email_address: string;
            id: string;
        }[];
        primary_email_address_id: string;
        first_name: string;
        last_name: string;
        primary_phone_number_id: string;
        phone_numbers: {
            phone_number: string;
            id: string;
        }[];
    }
    
    const webhookSecret: string = process.env.WEBHOOK_SECRET || "";
    
    export async function POST(req: Request) {
        const payload = await req.json()
        const payloadString = JSON.stringify(payload);
        const headerPayload = headers();
        const svixId = headerPayload.get("svix-id");
        const svixIdTimeStamp = headerPayload.get("svix-timestamp");
        const svixSignature = headerPayload.get("svix-signature");
        if (!svixId || !svixIdTimeStamp || !svixSignature) {
            console.log("svixId", svixId)
            console.log("svixIdTimeStamp", svixIdTimeStamp)
            console.log("svixSignature", svixSignature)
            return new Response("Error occured", {
                status: 400,
            })
        }
        const svixHeaders = {
            "svix-id": svixId,
            "svix-timestamp": svixIdTimeStamp,
            "svix-signature": svixSignature,
        };
        const wh = new Webhook(webhookSecret);
        let evt: Event | null = null;
        try {
            evt = wh.verify(payloadString, svixHeaders) as Event;
        } catch (_) {
            console.log("error")
            return new Response("Error occured", {
                status: 400,
            })
        }
        // Handle the webhook
        const eventType: EventType = evt.type;
        const { id, first_name, last_name, emailAddresses } = evt.data;
        if (eventType === "user.created") {
            const email = emailAddresses[0].emailAddress;
            try {
                await db.insert(users).values({
                    id,
                    first_name,
                    last_name,
                    email,
                });
                return new Response("OK", { status: 200 });
            } catch (error) {
                console.log(error);
                return new Response("Error handling user creation in the database", {
                    status: 400,
                })
            }
        } else if (eventType == "user.deleted") {
            try {
                await db.delete(users).where(eq(users.id, id));
                const recordsToDelete = (await db.select().from(playlists).leftJoin(users, eq(playlists.user_id, users.id)).where(isNull(users.id)));
                const idsToDelete = recordsToDelete.map(record => record.playlists.id);
                await db
                    .delete(playlists).where(inArray(playlists.id, idsToDelete));
                return new Response("OK", { status: 200 });
            } catch (error) {
                console.error(error);
                throw new Error(`Failed to insert user into database`);
            }
        } else {
            console.log("eventType", eventType)
            return new Response("Invalid event type", {
                status: 201,
            })
        }
    }
    
    
    type Event = {
        data: UserInterface;
        object: "event";
        type: EventType;
    };
    
    type EventType = "user.created" | "user.deleted" | "*";
    
  2. Set up the localtunnel or ngrok tunnel for webhook testing
  3. Set the WEBHOOK_SECRET environment variable
  4. Send a webhook to the https://13f1-...-859.ngrok-free.app/api/webhooks/clerk endpoint

Expected Behavior

The webhook should be processed successfully.

Actual Behavior

A 401 Unauthorized error is returned.

The 401 Unauthorized error suggests that the webhook is failing the authentication process. To resolve this issue, you should ensure that the WEBHOOK_SECRET environment variable is correctly set.

  1. Check that you have set the WEBHOOK_SECRET environment variable. You can do this by running echo $WEBHOOK_SECRET in the terminal. If it returns an empty value or an incorrect value, you need to set the correct value for the WEBHOOK_SECRET environment variable.

  2. Set the WEBHOOK_SECRET environment variable with the correct value. You can do this by running export WEBHOOK_SECRET=your_secret_value in the terminal, replacing your_secret_value with the actual secret value.

  3. After setting the WEBHOOK_SECRET environment variable, restart your application or server so that it can pick up the new value.

  4. Test the webhook again by sending a request to the https://13f1-...-859.ngrok-free.app/api/webhooks/clerk endpoint. The 401 Unauthorized error should no longer occur if the WEBHOOK_SECRET is set correctly.

If you continue to experience issues, ensure that the webhook secret value matches the one expected by the webhook verification process.