This is the short, practical guide for letting a vBulletin frontend hook talk directly to Tools after the forum registration appears to be complete.
The goal is to avoid sending the user through the full public onboarding page when the forum already knows the user and the invite code.
Public endpoint:
POST /api/vbulletin/onboarding/invite/completeExamples:
https://tools.tornevall.net/api/vbulletin/onboarding/invite/completehttps://tools.tornevall.com/api/vbulletin/onboarding/invite/completeThis endpoint uses no bearer tokens and should not be protected by a separate frontend token.
The safety model here instead depends on:
field66Request body:
{
"invite_code": "invite-code-here",
"forum_user_id": 123,
"forum_username": "snowrider",
"forum_email": "user@example.com"
}
Rules:
invite_code is requiredforum_user_id is the strongest extra identity hint when availableforum_username is a good fallbackforum_email is only a hint and is never treated as proof on its ownIf the forum-side invite field happens to contain a full URL instead of only the invite token, Tools now normalizes that value server-side.
That means this still works:
https://tools.tornevall.net/vbulletin/onboarding/snowboarding/ABC123
Tools will try to extract the actual invite code (ABC123) before running the lookup.
It is still smart to normalize the value client-side before sending it.
For the current vBulletin hook:
header_headsnowball_header_scriptHook arguments:
userinfo=userinfo
bbuserinfo=bbuserinfo
That gives the template access to normal user fields through both userinfo and bbuserinfo.
Your current bootstrap block should be adapted so it can handle both empty values and URL-based invite-field values.
Example:
<script>
(function () {
function firstNonEmpty() {
for (var i = 0; i < arguments.length; i += 1) {
var value = String(arguments[i] || '').trim();
if (value !== '') {
return value;
}
}
return '';
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || {};
window.TornevallTools.vbulletin.user.userid = firstNonEmpty(
"{vb:raw bbuserinfo.userid}",
"{vb:raw userinfo.userid}"
);
window.TornevallTools.vbulletin.user.username = firstNonEmpty(
"{vb:raw bbuserinfo.username}",
"{vb:raw userinfo.username}"
);
window.TornevallTools.vbulletin.user.invitationCodeRaw = firstNonEmpty(
"{vb:raw bbuserinfo.field66}",
"{vb:raw userinfo.field66}"
);
window.TornevallTools.vbulletin.user.invitationFieldId = 66;
})();
</script>
Use a small helper before making the API call.
<script>
(function () {
function normalizeInviteCode(rawValue) {
var value = String(rawValue || '').trim();
if (!value) {
return '';
}
try {
var parsed = new URL(value, window.location.origin);
var fromQuery = parsed.searchParams.get('invite_code')
|| parsed.searchParams.get('inviteKey')
|| parsed.searchParams.get('invite_key');
if (fromQuery) {
return String(fromQuery).trim();
}
var segments = parsed.pathname.split('/').filter(Boolean);
var onboardingIndex = segments.indexOf('onboarding');
if (onboardingIndex !== -1 && segments.length > onboardingIndex + 2) {
return String(segments[onboardingIndex + 2] || '').trim();
}
} catch (error) {
// Not a URL, keep the raw value.
}
return value;
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.normalizeInviteCode = normalizeInviteCode;
})();
</script>
This is the practical core.
<script>
(function () {
var toolsApiBase = 'https://tools.tornevall.net';
function normalizeInviteCode(rawValue) {
if (
window.TornevallTools
&& window.TornevallTools.vbulletin
&& typeof window.TornevallTools.vbulletin.normalizeInviteCode === 'function'
) {
return window.TornevallTools.vbulletin.normalizeInviteCode(rawValue);
}
return String(rawValue || '').trim();
}
function getForumUser() {
return (
window.TornevallTools
&& window.TornevallTools.vbulletin
&& window.TornevallTools.vbulletin.user
) || {};
}
function completeInviteWithTools() {
var user = getForumUser();
var inviteCode = normalizeInviteCode(user.invitationCodeRaw || user.invitationCode || '');
var forumUserId = parseInt(user.userid, 10) || 0;
var forumUsername = String(user.username || '').trim();
if (!inviteCode || !forumUserId) {
return Promise.resolve(null);
}
return fetch(toolsApiBase + '/api/vbulletin/onboarding/invite/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
invite_code: inviteCode,
forum_user_id: forumUserId,
forum_username: forumUsername
})
}).then(function (response) {
return response.json().then(function (data) {
return {
ok: response.ok,
status: response.status,
data: data
};
});
});
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.completeInviteWithTools = completeInviteWithTools;
})();
</script>
When the API says the access is actually complete (access_granted=true), you can show a clear popup.
<script>
(function () {
function showInviteSuccessPopup(message, continueUrl) {
var existing = document.getElementById('tools-invite-success-popup');
if (existing) {
existing.remove();
}
var overlay = document.createElement('div');
overlay.id = 'tools-invite-success-popup';
overlay.innerHTML = ''
+ '<div class="tools-invite-popup-backdrop"></div>'
+ '<div class="tools-invite-popup-card" role="dialog" aria-live="polite" aria-modal="true">'
+ ' <div class="tools-invite-popup-icon">✓</div>'
+ ' <h2>Congratulations!</h2>'
+ ' <p>' + (message || 'You have now been admitted to the forum.') + '</p>'
+ ' <div class="tools-invite-popup-actions">'
+ (continueUrl
? ' <a class="tools-invite-popup-button" href="' + continueUrl + '">Continue to the forum</a>'
: '')
+ ' <button type="button" class="tools-invite-popup-close">Close</button>'
+ ' </div>'
+ '</div>';
var style = document.createElement('style');
style.textContent = ''
+ '#tools-invite-success-popup { position: fixed; inset: 0; z-index: 2147483647; }'
+ '.tools-invite-popup-backdrop { position: absolute; inset: 0; background: rgba(10, 16, 25, 0.55); }'
+ '.tools-invite-popup-card {'
+ ' position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);'
+ ' width: min(92vw, 460px); padding: 28px 24px; border-radius: 18px;'
+ ' background: linear-gradient(180deg, #ffffff, #f5fbf7); box-shadow: 0 20px 60px rgba(0,0,0,0.25);'
+ ' text-align: center; font-family: Arial, sans-serif; color: #123226;'
+ '}'
+ '.tools-invite-popup-icon {'
+ ' width: 56px; height: 56px; margin: 0 auto 14px auto; border-radius: 50%;'
+ ' display: flex; align-items: center; justify-content: center;'
+ ' background: #2f9e68; color: #fff; font-size: 28px; font-weight: bold;'
+ '}'
+ '.tools-invite-popup-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }'
+ '.tools-invite-popup-button, .tools-invite-popup-close {'
+ ' appearance: none; border: 0; border-radius: 999px; padding: 12px 18px;'
+ ' background: #2f9e68; color: #fff; text-decoration: none; cursor: pointer; font-weight: 600;'
+ '}'
+ '.tools-invite-popup-close { background: #5b6770; }';
overlay.appendChild(style);
document.body.appendChild(overlay);
var closeButton = overlay.querySelector('.tools-invite-popup-close');
if (closeButton) {
closeButton.addEventListener('click', function () {
overlay.remove();
});
}
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.showInviteSuccessPopup = showInviteSuccessPopup;
})();
</script>
<script>
(function () {
function runInviteCompletionFlow() {
if (!window.TornevallTools || !window.TornevallTools.vbulletin) {
return;
}
var api = window.TornevallTools.vbulletin;
if (typeof api.completeInviteWithTools !== 'function') {
return;
}
api.completeInviteWithTools().then(function (result) {
if (!result || !result.ok || !result.data || !result.data.ok) {
return;
}
if (result.data.access_granted && typeof api.showInviteSuccessPopup === 'function') {
var continueUrl = result.data.request && result.data.request.public_status_url
? result.data.request.public_status_url
: 'https://forum.tornevall.net';
api.showInviteSuccessPopup(
result.data.message || 'Congratulations, you are now admitted.',
continueUrl
);
}
}).catch(function (error) {
if (window.console && console.warn) {
console.warn('Invite completion failed:', error);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runInviteCompletionFlow);
} else {
runInviteCompletionFlow();
}
})();
</script>
access_granted=truerequires_manual_review=true404 or 422header_head.userinfo=userinfobbuserinfo=bbuserinfofield66 value as invitationCodeRaw.This is the current complete example of how the activation can be done inside snowball_header_script.
Practical behavior in this example:
window.TornevallTools.vbulletin.userinviteCompleteEndpoint directly in the hookinvitationCodeRaw and the normalized invitationCode404 invite code not found as a stop state instead of repeating the same failure during the same session<script>
(function () {
window.TornevallTools = window.TornevallTools || new Object();
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || new Object();
window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || new Object();
window.TornevallTools.vbulletin.user.userid = "{vb:raw user.userid}";
window.TornevallTools.vbulletin.user.username = "{vb:raw user.username}";
window.TornevallTools.vbulletin.user.invitationCodeRaw = "{vb:raw user.field66}";
window.TornevallTools.vbulletin.user.invitationCode = "{vb:raw user.field66}";
window.TornevallTools.vbulletin.user.invitationFieldId = 66;
window.TornevallTools.vbulletin.user.inviteCompleteEndpoint = "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete";
})();
(function () {
var path = decodeURIComponent(window.location.pathname).toLowerCase();
if (path === "/snowball" || path === "/snowball/") {
window.location.replace("/snowball/forum");
return;
}
if (path === "/snowball/forum" || path.startsWith("/snowball/forum/")) {
document.documentElement.classList.add("is-snowball-forum");
}
})();
</script>
<style>
html.is-snowball-forum #header .site-logo a > img {
visibility: hidden !important;
}
#tools-vb-member-widgets {
max-width: 1080px;
margin: 0 auto 24px auto;
padding: 18px 22px;
border-left: 5px solid #6faf8b;
border-radius: 10px;
background: #f7fff9;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.08);
text-align: center;
font-weight: 600;
}
#tools-vb-member-widgets .tools-vb-widget-line {
display: block;
margin: 4px 0;
}
#tools-vb-widget-loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
opacity: 0.75;
}
.tools-vb-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(0, 0, 0, 0.18);
border-top-color: rgba(0, 0, 0, 0.65);
border-radius: 50%;
animation: toolsVbSpin 0.8s linear infinite;
}
@keyframes toolsVbSpin {
to {
transform: rotate(360deg);
}
}
#tools-snowball-autoverify-popup {
position: fixed;
right: 22px;
bottom: 22px;
z-index: 999999;
max-width: 420px;
padding: 18px 20px;
border-left: 5px solid #6faf8b;
border-radius: 12px;
background: #f7fff9;
color: #1d2b20;
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.22);
font-family: inherit;
line-height: 1.45;
opacity: 0;
transform: translateY(14px);
transition: opacity 180ms ease, transform 180ms ease;
}
#tools-snowball-autoverify-popup.is-visible {
opacity: 1;
transform: translateY(0);
}
#tools-snowball-autoverify-popup.is-error {
border-left-color: #d0051b;
background: #fff7f7;
color: #3a1414;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-title {
display: block;
margin-bottom: 6px;
padding-right: 24px;
font-size: 16px;
font-weight: 800;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-message {
display: block;
padding-right: 24px;
font-size: 14px;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-close {
position: absolute;
top: 7px;
right: 9px;
border: 0;
background: transparent;
color: inherit;
font-size: 20px;
line-height: 20px;
cursor: pointer;
opacity: 0.7;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-close:hover {
opacity: 1;
}
</style>
<link rel="preload" as="image" href="https://forum.tornevall.net/filedata/fetch?photoid=1063354" fetchpriority="high">
<link rel="preconnect" href="https://tools.tornevall.net" crossorigin>
<link rel="preload" as="script" href="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/latest-member.js?group_ids=155&variant=plain&container_id=tools-vb-latest-member-widget&template=Vi+v%C3%A4lkomnar+v%C3%A5r+senaste+medlem+%7Bname%7D">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/member-count.js?group_ids=155&variant=plain&container_id=tools-vb-member-count-widget&template=Sn%C3%B6bollseffekten+v%C3%A4xer+-+vi+%C3%A4r+nu+%7Bcount%7D+medlemmar">
<script defer src="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js"></script>
<script>
(function () {
var snowballLogoUrl = "https://forum.tornevall.net/filedata/fetch?photoid=1063354";
var path = decodeURIComponent(window.location.pathname).toLowerCase();
if (!(path === "/snowball/forum" || path.startsWith("/snowball/forum/"))) {
return;
}
var preloadedLogo = new Image();
preloadedLogo.fetchPriority = "high";
preloadedLogo.src = snowballLogoUrl;
function swapSnowballLogo() {
var logo = document.querySelector("#header .site-logo a > img");
if (!logo) {
return false;
}
logo.src = snowballLogoUrl;
logo.setAttribute("data-orig-src", snowballLogoUrl);
logo.alt = "Snöbollseffekten";
logo.title = "Snöbollseffekten";
logo.style.setProperty("visibility", "visible", "important");
return true;
}
if (!swapSnowballLogo()) {
var observer = new MutationObserver(function () {
if (swapSnowballLogo()) {
observer.disconnect();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
window.setTimeout(function () {
observer.disconnect();
}, 5000);
}
})();
(function () {
var user = window.TornevallTools
&& window.TornevallTools.vbulletin
&& window.TornevallTools.vbulletin.user
? window.TornevallTools.vbulletin.user
: new Object();
function trimString(value) {
return String(value || "").replace(/^\s+|\s+$/g, "");
}
function normalizeInviteCode(value) {
var raw = trimString(value);
if (!raw) {
return "";
}
try {
if (/^https?:\/\//i.test(raw)) {
var url = new URL(raw);
var parts = url.pathname.split("/");
var index = parts.length - 1;
while (index >= 0) {
if (parts[index]) {
return decodeURIComponent(parts[index]);
}
index--;
}
}
} catch (error) {
}
return raw;
}
function showSnowballPopup(title, message, isError) {
var existing = document.getElementById("tools-snowball-autoverify-popup");
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}
var popup = document.createElement("div");
popup.id = "tools-snowball-autoverify-popup";
if (isError) {
popup.className = "is-error";
}
var closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "tools-snowball-popup-close";
closeButton.setAttribute("aria-label", "Close");
closeButton.appendChild(document.createTextNode("×"));
var titleNode = document.createElement("span");
titleNode.className = "tools-snowball-popup-title";
titleNode.appendChild(document.createTextNode(title));
var messageNode = document.createElement("span");
messageNode.className = "tools-snowball-popup-message";
messageNode.appendChild(document.createTextNode(message));
closeButton.onclick = function () {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
};
popup.appendChild(closeButton);
popup.appendChild(titleNode);
popup.appendChild(messageNode);
document.body.appendChild(popup);
window.setTimeout(function () {
popup.className = popup.className
? popup.className + " is-visible"
: "is-visible";
}, 20);
window.setTimeout(function () {
if (popup && popup.parentNode) {
popup.className = popup.className.replace(/\bis-visible\b/g, "");
}
}, 9000);
window.setTimeout(function () {
if (popup && popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 9500);
}
function parseJsonResponse(response) {
return response.text().then(function (text) {
var payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch (error) {
payload = {
ok: false,
message: text || "Empty response",
raw: text || ""
};
}
if (!payload) {
payload = new Object();
}
payload.httpStatus = response.status;
payload.httpOk = response.ok;
return payload;
});
}
function isInviteNotFound(data) {
var message = trimString(data && data.message ? data.message : "").toLowerCase();
return data
&& Number(data.httpStatus) === 404
&& (
message === "invite code not found."
|| message === "invite code not found"
|| message.indexOf("invite code not found") !== -1
);
}
function isSuccessResponse(data) {
return data
&& (
data.ok === true
|| data.success === true
|| data.completed === true
|| data.status === "completed"
|| data.status === "ok"
|| data.status === "success"
);
}
function runSnowballAutoverify() {
var endpoint = trimString(user.inviteCompleteEndpoint || "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete");
var userid = trimString(user.userid || "");
var username = trimString(user.username || "");
var inviteCodeRaw = trimString(user.invitationCodeRaw || user.invitationCode || "");
var inviteCode = normalizeInviteCode(inviteCodeRaw);
if (window.console && console.log) {
console.log("Snowball autoverify boot:", {
endpoint: endpoint,
userid: userid,
username: username,
inviteCodeRaw: inviteCodeRaw,
inviteCode: inviteCode
});
}
if (!endpoint) {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing endpoint");
}
return;
}
if (!userid || userid === "0") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing userid");
}
return;
}
if (!inviteCode) {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing invite code");
}
return;
}
var storageKey = "tools_snowball_invite_complete_" + userid + "_" + inviteCode;
try {
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "done") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: already done in this browser session");
}
return;
}
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "not_found") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: invite code was already reported as not found in this browser session");
}
return;
}
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "running") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: already running");
}
return;
}
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "running");
}
} catch (error) {
}
var body = new FormData();
body.append("invite_code", inviteCode);
body.append("invite_code_raw", inviteCodeRaw);
body.append("forum_user_id", userid);
body.append("forum_username", username);
body.append("userid", userid);
body.append("username", username);
body.append("field_id", String(user.invitationFieldId || 66));
if (window.console && console.log) {
console.log("Snowball autoverify POST:", endpoint);
}
fetch(endpoint, {
method: "POST",
body: body,
credentials: "omit"
})
.then(parseJsonResponse)
.then(function (data) {
if (window.console && console.log) {
console.log("Snowball autoverify response:", data);
}
if (isSuccessResponse(data)) {
try {
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "done");
}
} catch (error) {
}
showSnowballPopup(
"Congratulations, you are now admitted!",
"Your invite code was verified and the forum access has been updated.",
false
);
return;
}
if (isInviteNotFound(data)) {
try {
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "not_found");
}
} catch (error) {
}
if (window.console && console.info) {
console.info("Snowball autoverify stopped: invite code not found in Tools.", {
inviteCode: inviteCode,
httpStatus: data.httpStatus,
message: data.message || ""
});
}
return;
}
try {
if (window.sessionStorage) {
sessionStorage.removeItem(storageKey);
}
} catch (error) {
}
if (window.console && console.info) {
console.info("Snowball autoverify did not complete:", data);
}
showSnowballPopup(
"The invite could not be verified",
data && data.message ? data.message : "Tools API responded, but did not confirm the admission.",
true
);
})
.catch(function (error) {
try {
if (window.sessionStorage) {
sessionStorage.removeItem(storageKey);
}
} catch (storageError) {
}
if (window.console && console.warn) {
console.warn("Snowball autoverify network error:", error);
}
showSnowballPopup(
"The invite could not be verified",
"The network call to Tools could not be completed right now.",
true
);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", runSnowballAutoverify);
} else {
runSnowballAutoverify();
}
})();
</script>
invitationCodeRaw and the normalized invite code.