K
Kinde4mo ago
COACH

Self-serve portal link without a Kinde SDK

Can someone help me understand where I am going wrong with generating a self-serve portal link? I use Netlify and have paths to call my Express app. Authentication all works, but I can't seem to generate a link for the self-serve portal. Netlify function error
Aug 21, 01:40:03 PM: 918de926 Duration: 4.61 ms Memory Usage: 146 MB
Aug 21, 01:40:13 PM: e57788d1 INFO GET /portal - User: anonymous
Aug 21, 01:40:13 PM: e57788d1 INFO Got the keys
Aug 21, 01:40:13 PM: e57788d1 INFO Token verification successful
Aug 21, 01:40:14 PM: e57788d1 INFO Access token type: string
Aug 21, 01:40:14 PM: e57788d1 INFO Access token (first 50 chars): eyJhbGciOiJSUzI1NiIsImtpZCI6IjlmOjYzOjVmOjg4OjczOm
Aug 21, 01:40:14 PM: e57788d1 ERROR Portal API error: {"errors":[{"code":"INVALID_CREDENTIALS","message":"Invalid credentials used to access API"},{"code":"MISSING_AUDIENCE","message":"Missing or incorrect Management API audience"}]}
Aug 21, 01:40:14 PM: e57788d1 Duration: 675.77 ms Memory Usage: 146 MB
Aug 21, 01:40:14 PM: 4a9bb456 INFO GET /access/ - User: anonymous
Aug 21, 01:40:14 PM: 4a9bb456 Duration: 6.58 ms Memory Usage: 146 MB
Aug 21, 01:40:03 PM: 918de926 Duration: 4.61 ms Memory Usage: 146 MB
Aug 21, 01:40:13 PM: e57788d1 INFO GET /portal - User: anonymous
Aug 21, 01:40:13 PM: e57788d1 INFO Got the keys
Aug 21, 01:40:13 PM: e57788d1 INFO Token verification successful
Aug 21, 01:40:14 PM: e57788d1 INFO Access token type: string
Aug 21, 01:40:14 PM: e57788d1 INFO Access token (first 50 chars): eyJhbGciOiJSUzI1NiIsImtpZCI6IjlmOjYzOjVmOjg4OjczOm
Aug 21, 01:40:14 PM: e57788d1 ERROR Portal API error: {"errors":[{"code":"INVALID_CREDENTIALS","message":"Invalid credentials used to access API"},{"code":"MISSING_AUDIENCE","message":"Missing or incorrect Management API audience"}]}
Aug 21, 01:40:14 PM: e57788d1 Duration: 675.77 ms Memory Usage: 146 MB
Aug 21, 01:40:14 PM: 4a9bb456 INFO GET /access/ - User: anonymous
Aug 21, 01:40:14 PM: 4a9bb456 Duration: 6.58 ms Memory Usage: 146 MB
 Code
/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
// Get access token from the user's session via Kinde client
const accessToken = await kindeClient.getToken(req);
console.log("Access token type:", typeof accessToken);
console.log("Access token (first 50 chars):", accessToken ? accessToken.substring(0, 50) : "null");

if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}

// Generate portal link using Kinde Account API
const portalResponse = await fetch(
`${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
return_url: `${baseUrl}/access/`,
}),
},
);

if (!portalResponse.ok) {
console.error("Portal API error:", await portalResponse.text());
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}

const portalData = await portalResponse.json();

// Redirect user to the portal
res.redirect(portalData.url);
} catch (error) {
console.error("Portal generation error:", error);
res.redirect(`${baseUrl}/access/?error=portal-error`);
}
});
/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
// Get access token from the user's session via Kinde client
const accessToken = await kindeClient.getToken(req);
console.log("Access token type:", typeof accessToken);
console.log("Access token (first 50 chars):", accessToken ? accessToken.substring(0, 50) : "null");

if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}

// Generate portal link using Kinde Account API
const portalResponse = await fetch(
`${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
return_url: `${baseUrl}/access/`,
}),
},
);

if (!portalResponse.ok) {
console.error("Portal API error:", await portalResponse.text());
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}

const portalData = await portalResponse.json();

// Redirect user to the portal
res.redirect(portalData.url);
} catch (error) {
console.error("Portal generation error:", error);
res.redirect(`${baseUrl}/access/?error=portal-error`);
}
});
4 Replies
Abdelrahman Zaki - Kinde
Hey @COACH, you're using the user access token to call the Account API - that’s right. But the issue is with the URL you're using. You're calling:
${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link
${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link
But for user access tokens, it should be:
${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link
${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link
Just switch to that and it should work. Let me know if you hit any issues!
COACH
COACHOP4mo ago
So getting...
Aug 22, 12:21:05 PM: 754a753f ERROR Portal API error: [{"Code":"ROUTE_NOT_FOUND","Message":"Incorrect API route or method"}]
Aug 22, 12:21:05 PM: 754a753f ERROR Portal API error: [{"Code":"ROUTE_NOT_FOUND","Message":"Incorrect API route or method"}]
In the documentation: https://docs.kinde.com/build/set-up-options/self-serve-portal-for-users/ the url is my original one...
const response = await fetch("/api/v1/account_api/portal_link", {
headers: {
Authorization: `Bearer ${userAccessToken}`
}
});
const data = await response.json();
window.location = data.url;
const response = await fetch("/api/v1/account_api/portal_link", {
headers: {
Authorization: `Bearer ${userAccessToken}`
}
});
const data = await response.json();
window.location = data.url;
OK this is working now! Posting for reference.
/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
const accessToken = await kindeClient.getToken(req);
if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}

const response = await fetch(
`${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link?return_url=${encodeURIComponent(`${baseUrl}/access/`)}`,
{
headers: { Authorization: `Bearer ${accessToken}` },
signal: AbortSignal.timeout(10000)
}
);

if (!response.ok) {
console.error(`Portal API error: ${response.status}`);
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}

const data = await response.json();
if (!data?.url) {
console.error("Portal API returned invalid response");
return res.redirect(`${baseUrl}/access/?error=invalid-response`);
}

res.redirect(data.url);
} catch (error) {
console.error("Portal error:", error);
res.redirect(`${baseUrl}/access/`);
}
});
/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
const accessToken = await kindeClient.getToken(req);
if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}

const response = await fetch(
`${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link?return_url=${encodeURIComponent(`${baseUrl}/access/`)}`,
{
headers: { Authorization: `Bearer ${accessToken}` },
signal: AbortSignal.timeout(10000)
}
);

if (!response.ok) {
console.error(`Portal API error: ${response.status}`);
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}

const data = await response.json();
if (!data?.url) {
console.error("Portal API returned invalid response");
return res.redirect(`${baseUrl}/access/?error=invalid-response`);
}

res.redirect(data.url);
} catch (error) {
console.error("Portal error:", error);
res.redirect(`${baseUrl}/access/`);
}
});
Abdelrahman Zaki - Kinde
Awesome - glad it’s working now and thanks for sharing the final code! You’re right about the docs - we’ll raise a fix to update the endpoint to /account_api/v1/portal_link. Appreciate you flagging it!
Abdelrahman Zaki - Kinde
Hey @COACH, just a quick update to let you know we’ve now updated the docs to reflect the correct endpoint. Appreciate you flagging it and sharing your working example! Let us know if anything else comes up.

Did you find this page helpful?