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`);
}
});
4 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!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.