← Back to docs

vBulletin invite-complete hook without the onboarding page

Language: EN | EN | SV

vBulletin invite-complete hook without the onboarding page

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.

Endpoint

Public endpoint:

  • POST /api/vbulletin/onboarding/invite/complete

Examples:

  • production: https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete
  • dev/test: https://tools.tornevall.com/api/vbulletin/onboarding/invite/complete

Important auth note

This endpoint uses no bearer tokens and should not be protected by a separate frontend token.

The safety model here instead depends on:

  • a real invite code
  • optional forum identity hints
  • server-side Tools validation that the resolved forum account really matches the invite
  • extra profile-field validation when the onboarding flow uses an invite field such as field66

What the endpoint accepts

Request body:

{
  "invite_code": "invite-code-here",
  "forum_user_id": 123,
  "forum_username": "snowrider",
  "forum_email": "user@example.com"
}

Rules:

  • invite_code is required
  • forum_user_id is the strongest extra identity hint when available
  • forum_username is a good fallback
  • forum_email is only a hint and is never treated as proof on its own

Full URL in the invite field? Tools now strips it server-side

If 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.

Recommended hook configuration

For the current vBulletin hook:

  • Hook location: header_head
  • Template name: snowball_header_script

Hook arguments:

userinfo=userinfo
bbuserinfo=bbuserinfo

That gives the template access to normal user fields through both userinfo and bbuserinfo.

Adapted user bootstrap

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>

Recommended client-side invite-code normalization

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>

Example: send completion directly to Tools

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>

Example: styled success popup when access is really granted

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>

Example: the full flow together

<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>

Recommended behavior after the API response

When access_granted=true

  • show the popup immediately
  • link onward to the forum or the personal status page
  • do not use the onboarding page as an extra middle step

When requires_manual_review=true

  • show a neutral information box instead of a congratulations popup
  • explain that the registration was received and now waits for review

When the response is 404 or 422

  • log the failure in the console first
  • do not show a false success popup
  • first verify that the correct invite code really existed in the profile field

Short manual

  1. Put the hook in header_head.
  2. Pass the hook arguments:
    • userinfo=userinfo
    • bbuserinfo=bbuserinfo
  3. Store the raw field66 value as invitationCodeRaw.
  4. Normalize that value before the POST.
  5. Send only the invite code plus forum identity to Tools.
  6. Do not send bearer tokens.
  7. Only show the success popup when the API actually confirms that access was granted.

Finished example: current Snowball autoverify that works

This is the current complete example of how the activation can be done inside snowball_header_script.

Practical behavior in this example:

  • sets up window.TornevallTools.vbulletin.user
  • keeps inviteCompleteEndpoint directly in the hook
  • keeps both invitationCodeRaw and the normalized invitationCode
  • tries to verify the invite code automatically after page load
  • stops if the invite code is missing or the same user/code was already handled in the current browser session
  • shows a styled popup for both success and failure
  • treats 404 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>

Why this example is useful

  • It shows the full real hook flow in one piece.
  • It uses the same public endpoint that Tools now documents.
  • It shows the difference between invitationCodeRaw and the normalized invite code.
  • It shows how autoverify can stay quiet when the invite code does not exist, while still giving a clear popup when a real admission or a real error happens.