TryGhost/Ghost · error · Error
No invitation token found in URL for ${email}
Error message
No invitation token found in URL for ${email} What it means
An invitation email was found and fetched in detail, but extractInviteLink() returned a URL whose pathname split into segments has no trailing token (the last non-empty segment is empty/missing). Ghost invitation URLs look like /ghost/#/invitation/<token>; if the link is malformed, truncated, or points elsewhere, no token can be extracted.
Source
Thrown at e2e/helpers/playwright/fixture.ts:214
try {
messages = await emailClient.search({subject: 'has invited you to join', to: email});
} catch (error) {
const detail = error instanceof Error ? `: ${error.message}` : '';
throw new Error(`No staff invitation email found for ${email}${detail}`);
}
const message = messages[0];
if (!message) {
throw new Error(`No staff invitation email found for ${email}`);
}
const detailedMessage = await emailClient.getMessageDetailed(message);
const inviteUrl = new URL(extractInviteLink(detailedMessage));
const token = inviteUrl.pathname.split('/').filter(Boolean).at(-1);
if (!token) {
throw new Error(`No invitation token found in URL for ${email}`);
}
return token;
}
async function createStaffAccount(page: Page, emailClient: EmailClient, role: AssignableStaffRoleName): Promise<StaffAccount> {
const staffAccountFactory = createStaffAccountFactory(
page.request,
email => getInvitationToken(emailClient, email)
);
return await staffAccountFactory.create({role});
}
/**
* Playwright fixture that provides a unique Ghost instance for each test
* Each instance gets its own database, runs on a unique port, and includes authentication
*View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Log inviteUrl.pathname and the extracted link to see the actual shape; update extractInviteLink() if the template changed.
- Make token extraction robust: check pathname segments, fall back to a search param (e.g. token=), decode URL first.
- Confirm the email found is actually the staff-invite email (not a related notification) by checking the sender/subject.
- Re-run invitation flow and inspect the raw email HTML to locate the real link.
Example fix
// before
const token = inviteUrl.pathname.split('/').filter(Boolean).at(-1);
if (!token) throw new Error(`No invitation token found in URL for ${email}`);
// after
const token = inviteUrl.pathname.split('/').filter(Boolean).at(-1)
|| inviteUrl.searchParams.get('token')
|| inviteUrl.hash.match(/invitation\/([\w-]+)/)?.[1];
if (!token) throw new Error(`No invitation token found in URL for ${email}: ${inviteUrl.toString()}`); Defensive patterns
Strategy: validation
Validate before calling
function extractToken(url: URL): string | undefined {
return url.pathname.split('/').filter(Boolean).at(-1)
|| url.searchParams.get('token')
|| url.hash.match(/invitation\/([\w-]+)/)?.[1];
}
if (!extractToken(inviteUrl)) {
throw new Error(`No token in invite URL: ${inviteUrl.toString()}`);
} Type guard
function hasInvitationToken(url: URL): boolean {
return Boolean(
url.pathname.split('/').filter(Boolean).at(-1)
|| url.searchParams.get('token')
|| url.hash.match(/invitation\/[\w-]+/)
);
} Prevention
- Make token extraction robust across pathname, search param, and hash shapes.
- Log the full invite URL on failure to catch template drift early.
- Confirm extractInviteLink() selects the staff-invite link, not a footer/unsubscribe link.
When it happens
Trigger: The email body's invite link is malformed (missing the token segment). extractInviteLink() grabbed the wrong anchor (e.g. an unsubscribe link). Email template changed so the token lives in a different URL shape. The link uses a query param instead of a path segment.
Common situations: Ghost invitation template changed URL structure; email client renders text-only and the link is wrapped; extraction regex/select picks the wrong link; invitation was for a different role/flow with a different URL.
Related errors
- No staff invitation email found for ${email}${detail}
- No staff invitation email found for ${email}
- No sign-in email found for ${member.email}
- Timeout after ${timeoutMs}ms waiting for search results
- Failed to ${callType}: ${response.statusText}
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/1b141c4cb6d230eb.
Report an issue: GitHub.