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
Code
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
/**
* 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`);
}
});
3 Replies
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:
But for user access tokens, it should be:
Just switch to that and it should work. Let me know if you hit any issues!
${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link
${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link
${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link
${kindeConfig.issuerBaseUrl}/account_api/v1/portal_link
So getting...
In the documentation: https://docs.kinde.com/build/set-up-options/self-serve-portal-for-users/ the url is my original one...
OK this is working now!
Posting for reference.
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"}]
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;
/**
* 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/`);
}
});
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!