Budibase/budibase · error · HTTPError
Link confirmation is invalid or has expired
Error message
Link confirmation is invalid or has expired
What it means
confirmChatLinkSession validates the chat identity link confirmation step. The server stored a confirmationToken and confirmationGlobalUserId on the session during the handoff stage; this check requires the session to still hold both, that the stored user matches the currently authenticated global user, and that the confirmationToken in the request body matches the stored one. Any mismatch means the confirmation cannot be trusted, so the endpoint refuses with a 400.
Source
Thrown at packages/server/src/api/controllers/ai/chatIdentityLinks.ts:277
throw new HTTPError("Link token is invalid or has expired", 400)
}
assertSessionMatchesInstance({
workspaceId: session.workspaceId,
instance: ctx.params.instance,
})
if (!ctx.isAuthenticated) {
throw new HTTPError("Authentication is required to link chat identity", 401)
}
const currentGlobalUserId = getCurrentGlobalUserId(ctx)
if (
!session.confirmationToken ||
!session.confirmationGlobalUserId ||
session.confirmationGlobalUserId !== currentGlobalUserId ||
ctx.request.body?.confirmationToken !== session.confirmationToken
) {
throw new HTTPError("Link confirmation is invalid or has expired", 400)
}
const consumedSession =
await sdk.ai.chatIdentityLinks.consumeChatIdentityLinkSession(token)
if (!consumedSession) {
throw new HTTPError("Link token is invalid or has expired", 400)
}
assertSessionMatchesInstance({
workspaceId: consumedSession.workspaceId,
instance: ctx.params.instance,
})
await sdk.ai.chatIdentityLinks.upsertChatIdentityLink({
provider: consumedSession.provider,
externalUserId: consumedSession.externalUserId,
externalUserName: consumedSession.externalUserName,
teamId: consumedSession.teamId,
providerTenantId: consumedSession.providerTenantId,View on GitHub (pinned to a81a902e9a)
Solutions
- Restart the flow: navigate to the handoff URL again while logged in as the intended user so a fresh confirmationToken is generated and rendered into the confirmation page.
- Verify the browser session posting the confirmation is the same user who rendered the confirmation page (same cookie jar, no account switch).
- Check that the confirmation form submits the hidden confirmationToken field in the POST body exactly as rendered.
- If links keep expiring quickly, investigate session TTL in chatIdentityLinks SDK (storage/expiration) or clock skew between app servers.
- Clear stale link sessions via the SDK (delete/recreate the chat identity link session) and retry.
Example fix
// before (client posting without token)
await fetch(`/api/chat-links/${instance}/${token}/handoff`, { method: "POST" })
// after
const html = await (await fetch(`/api/chat-links/${instance}/${token}/handoff`)).text()
const confirmationToken = html.match(/name="confirmationToken" value="([^"]+)"/)?.[1]
await fetch(`/api/chat-links/${instance}/${token}/handoff`, {
method: "POST",
body: JSON.stringify({ confirmationToken }),
}) Defensive patterns
Strategy: validation
Validate before calling
const isValid = (session, body, currentGlobalUserId) =>
Boolean(session?.confirmationToken) &&
Boolean(session?.confirmationGlobalUserId) &&
session.confirmationGlobalUserId === currentGlobalUserId &&
body?.confirmationToken === session.confirmationToken
if (!isValid(session, requestBody, userId)) throw new Error("invalid confirmation") Type guard
const hasConfirmation = (s: { confirmationToken?: string; confirmationGlobalUserId?: string }, body: unknown, userId: string): s is { confirmationToken: string; confirmationGlobalUserId: string } =>
typeof s.confirmationToken === "string" &&
s.confirmationGlobalUserId === userId &&
typeof body === "object" && body !== null &&
(body as { confirmationToken?: unknown }).confirmationToken === s.confirmationToken Try / catch
try {
await api.post(`/api/chat-links/${instance}/${token}/handoff`, { confirmationToken })
} catch (e) {
if (e.status === 400) await restartHandoffFlow(token) // re-render confirmation page for a fresh token
} Prevention
- Always complete the confirmation in the same browser session and account that rendered the page
- Never cache or replay confirmationToken values across flows
- Disable double-submit on the confirmation form
- Re-fetch the handoff page to regenerate the token if any step fails
When it happens
Trigger: POST to /api/chat-links/:instance/:token/handoff (confirm) where the body omits confirmationToken, supplies a wrong or stale token, the session was never prepared via prepareChatIdentityLinkSessionConfirmation, or the session was prepared for a different logged-in user than the one confirming.
Common situations: A different Budibase user completes the confirmation page than the one who started the handoff; the confirmation page is loaded twice and an old CSRF-style token is replayed after re-preparation; the link expires between rendering the confirmation HTML and submitting; a client script posts without the hidden confirmationToken input.
Related errors
- Invalid bookmark query
- Invalid limit query
- Limit query must be between 1 and 100
- Invalid ${queryName} query
- Invalid environment query
AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29).
Data as JSON: /api/errors/8cdfe8451a064079.
Report an issue: GitHub.