Integration access token invalidates immediately

I am working on building a Vercel integration but I’m running into some problems:

When connecting an account to the integration, there is a popup window from the callback. This should exchange the code and config_id for an access token allowing me to use the Vercel api further.

The problem here is that despite me getting an access_token and an installation_id, as well as navigating to the next url. The installation gets cancelled and the popup never closes. I believe this invalidates the token.

I tested the token using postman but i get an Error 403, with a verbose output telling me that the token is invalid.

Questions here are:

  1. Why does the integration never complete installation
  2. Why is the access token not working
  3. Why does the window popup never close when navigated to next. Eventhough, the nexturl says it will close automatically?

The main callback page is shown below with the accompanying api route.

import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";

export default function VercelCallback() {
    const searchParams = useSearchParams();
    const code = searchParams.get("code");
    const configurationId = searchParams.get("configurationId");
    const next = searchParams.get("next");

    const [accessToken, setAccessToken] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        if (!code || !configurationId) return;
        const fetchAccessToken = async () => {
            try {
                const response = await fetch(
                    `/api/vercel/callback?code=${code}&configurationId=${configurationId}&next=${encodeURIComponent(next || "")}`
                );
                const data = await response.json();
                if (response.ok) {
                    setAccessToken(data.access_token);
                } else {
                    setError(data.error);
                }
            } catch (err) {
                setError("Failed to fetch access token.");
            }
        };

        fetchAccessToken();
        window.location.href = next;
    }, [code, configurationId, next]);

    return (
        <div>
            <h1>Vercel Callback</h1>
            {error && <p>Error: {error}</p>}
            {accessToken ? (
                <div>
                    <p>Access Token: {accessToken}</p>
                </div>
            ) : (
                <p>Loading...</p>
            )}
        </div>
    );
}
import nc from "next-connect";
import { ncOpts } from "@/api-lib/nc";

const handler = nc(ncOpts);

handler.get(async (req, res) => {
    const { code, configurationId } = req.query;

    if (!code) {
        return res.status(400).json({ error: "Missing authorization code." });
    }
    if (!configurationId) {
        return res.status(400).json({ error: "Missing configuration ID." });
    }

    const clientId = process.env.VERCEL_CLIENT_ID;
    const clientSecret = process.env.VERCEL_CLIENT_SECRET;
    const redirectUri = process.env.VERCEL_REDIRECT_URI;

    // Create URL-encoded body
    const body = new URLSearchParams();
    body.append("code", code);
    body.append("configurationId", configurationId);
    body.append("client_id", clientId);
    body.append("client_secret", clientSecret);
    body.append("redirect_uri", redirectUri);

    try {
        const response = await fetch("https://api.vercel.com/v2/oauth/access_token", {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: body.toString(),
        });

        const data = await response.json();

        if (!response.ok) {
            return res.status(response.status).json({ error: data.error || "Unknown error" });
        }

        console.log("Access Token Data:", data);
        const { access_token, team_id, user_id, installation_id } = data;
        
        return res.status(200).json({
            access_token,
            teamId: team_id,
            userId: user_id,
            installationId: installation_id
        });
    } catch (error) {
        console.error("Error in callback handler:", error);
        return res.status(500).json({ error: "Failed to fetch access token or team slug." });
    }
});

export default handler;