← Back to lessons
DISCORD SYSTEMS

Lesson — Verification System

Goal: Build a Roblox verification system using Dock Systems API — users link their Roblox account to gain access to the server.

Lesson Overview

Video Lesson

Notes — Understanding the Verification System

What is Dock Systems?

Dock Systems (docksys.xyz) is a third-party API that links Discord accounts to Roblox accounts. When a user connects their Roblox on the Dock Systems website, the API can return their Roblox ID and username given their Discord ID. This is how the bot knows who someone is on Roblox without asking them to type their name.

The API Key (config.json)

The DOCK_API key in config.json is your private authorization token for Dock Systems. It gets loaded onto client.config when the bot starts, so every file can access it via client.config.DOCK_API. Never hardcode this key directly in your code — always read it from config.

Two API Calls in docksystem.js

The getRobloxInfo() utility makes two separate requests:

  1. Dock Systems API — converts the Discord user ID → Roblox ID using your API key.
  2. Roblox API — uses that Roblox ID to fetch the username from users.roblox.com. This is a public API, no key needed.

Both calls are wrapped in a try/catch so if either fails, it returns an error object instead of crashing the bot.

deferReply vs deferUpdate

deferReply({ ephemeral: true }) — used in startVerify.js. This tells Discord "I'm thinking, send a private placeholder." The user sees a loading state only visible to them.

deferUpdate() — used in continueVerify.js. This acknowledges a button click without replacing the original message yet. You then call editReply() to update it. Required because API calls take time and Discord times out interactions after 3 seconds.

Components V2 (ContainerBuilder)

This system uses Discord's Components V2 — a newer layout system using ContainerBuilder, TextDisplayBuilder, SectionBuilder, and MediaGalleryBuilder. You must pass the MessageFlags.IsComponentsV2 flag on every message that uses these or Discord will reject it.

SectionBuilder with Button Accessory

In verify.js, the panel uses a SectionBuilder with .setButtonAccessory(). This places a button inline beside text rather than below it in an ActionRow — a V2-exclusive layout feature that looks much cleaner than a standard row.

The customID Flow

Each button has a customId that your bot listens for. The flow is:

start-verify → startVerify.js → shows account confirmation
continue-verify → continueVerify.js → assigns roles + logs

The button IDs must match exactly what you pass to setCustomId(). One typo = button does nothing.

Role Assignment

In continueVerify.js, two role IDs are defined: ADD_ROLE_ID (the verified role) and REMOVE_ROLE_ID (the unverified/gate role). On success, the bot adds one and removes the other. Both use .catch(() => {}) to silently ignore errors if the user already has or doesn't have the role — preventing crashes.

Log Embed

After verifying, the bot sends an EmbedBuilder to a private log channel. This uses Math.floor(Date.now() / 1000) to convert JavaScript's millisecond timestamps into Discord's Unix format (<t:...:R>) for relative timestamps like "5 minutes ago." The "View Profile" button is a Link button — link buttons don't have customIds, they just open a URL.

Step-by-Step

Set Up config.json

Add your Dock Systems API key to config.json. Your bot should load this file on startup and attach it to client.config.

Create the Dock Systems Utility

Build docksystem.js — the utility that calls the Dock Systems API and the Roblox API. Export getRobloxInfo() so any file can import and use it.

Send the Verification Panel

Create verify.js as a prefix command. When triggered, it deletes the command message and sends the verification panel to the channel using Components V2 with the start-verify button.

Handle Button: start-verify

In startVerify.js, defer the reply as ephemeral, call getRobloxInfo(), then show the user their linked account with Continue / Change Account buttons.

// startVerify.js — key pattern
await interaction.deferReply({ ephemeral: true });

const data = await getRobloxInfo(interaction.user.id, interaction, client);

if (!data || data.error) {
    return interaction.reply({ content: "Couldn't find your Roblox account.", flags: MessageFlags.Ephemeral });
}

// Show confirmation panel with Continue / Change Account buttons
await interaction.reply({ flags: MessageFlags.IsComponentsV2, ephemeral: true, components });

Handle Button: continue-verify

In continueVerify.js, defer the update, re-fetch their info, add/remove roles, edit the reply to show success, then send a log embed to your log channel.

// continueVerify.js — key pattern
await interaction.deferUpdate();

await member.roles.add(ADD_ROLE_ID).catch(() => {});
await member.roles.remove(REMOVE_ROLE_ID).catch(() => {});

await interaction.editReply({ flags: MessageFlags.IsComponentsV2, components });

// Send log embed to log channel
await logChannel.send({ embeds: [logEmbed], components: [actionRow] });

Full Verification System Code

Config — config.json

Show Full File
{
    "DOCK_API": "YOUR_DOCK_API_KEY_HERE"
}

API Utility — docksystem.js

Show Full File
const axios = require("axios");

async function getRobloxInfo(userId, interaction, client) {
    try {
        // Step 1: Discord ID → Roblox ID via Dock Systems
        const res = await axios.get(
            "https://api.docksys.xyz/api/v1/public/discord-to-roblox",
            {
                headers: {
                    Authorization: `Bearer ${client.config.DOCK_API}`,
                },
                params: {
                    discordId: userId,
                    guildId: interaction.guild.id,
                },
            },
        );

        if (!res.data || !res.data.data || !res.data.data.robloxId) {
            return { error: "No Roblox ID found for this Discord user." };
        }

        const robloxId = res.data.data.robloxId;

        // Step 2: Roblox ID → Username via public Roblox API
        const res2 = await axios.get(
            `https://users.roblox.com/v1/users/${robloxId}`,
        );

        const username = res2.data.name;

        return { robloxId, username };
    } catch (err) {
        console.error("Error in getRobloxInfo:", err.response?.data || err.message);
        return { error: "Failed to fetch Roblox info." };
    }
}

module.exports = { getRobloxInfo };

Verification Panel — verify.js

Show Full File
const {
    MediaGalleryBuilder,
    MediaGalleryItemBuilder,
    TextDisplayBuilder,
    SeparatorBuilder,
    SeparatorSpacingSize,
    ContainerBuilder,
    MessageFlags,
    ButtonBuilder,
    ButtonStyle,
    SectionBuilder,
} = require("discord.js");

module.exports = {
    name: "verify",
    description: "Sends the verification panel.",
    async execute(message) {
        await message.delete();

        const components = [
            new ContainerBuilder()
                .addMediaGalleryComponents(
                    new MediaGalleryBuilder().addItems(
                        new MediaGalleryItemBuilder().setURL(
                            "https://media.discordapp.net/attachments/1456184292983177349/1456768543335911424/Verification.png?ex=695990ee&is=69583f6e&hm=206f896b015af597f73dc59e475070e172d4e3b755c1b9f9b5ed48ea25e1ed59&=&format=webp&quality=lossless&width=1315&height=383"
                        )
                    )
                )
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent("### Verification Panel")
                )
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent(
                        "At **Ascend**, we use **Dock Systems** to verify users. In order to gain access to the rest of the server, you must verify your Roblox account. Please click the **verification** button below this message to get started in the verification process."
                    )
                )
                .addSeparatorComponents(
                    new SeparatorBuilder()
                        .setDivider(true)
                        .setSpacing(SeparatorSpacingSize.Small)
                )
                .addSectionComponents(
                    new SectionBuilder()
                        .addTextDisplayComponents(
                            new TextDisplayBuilder().setContent(
                                "Verify using the button here:"
                            )
                        )
                        .setButtonAccessory(
                            new ButtonBuilder()
                                .setCustomId("start-verify")
                                .setLabel("Begin Verification")
                                .setStyle(ButtonStyle.Secondary)
                        )
                ),
        ];

        await message.channel.send({
            flags: MessageFlags.IsComponentsV2,
            components,
        });
    },
};

Button Handler — startVerify.js

Show Full File
const {
    MessageFlags,
    ContainerBuilder,
    TextDisplayBuilder,
    SeparatorBuilder,
    SeparatorSpacingSize,
    ButtonBuilder,
    ButtonStyle,
    ActionRowBuilder,
} = require("discord.js");
const { getRobloxInfo } = require("../Utils/docksystem");

module.exports = {
    customID: "start-verify",
    async execute(interaction, client) {
        await interaction.deferReply({ ephemeral: true });

        const data = await getRobloxInfo(
            interaction.user.id,
            interaction,
            client
        );

        if (!data || data.error) {
            return interaction.reply({
                content:
                    "Couldn't verify your Roblox account. Make sure your Discord is linked in Docksys.",
                flags: MessageFlags.Ephemeral,
            });
        }

        const { robloxId, username } = data;

        const profileLink = `https://www.roblox.com/users/${robloxId}/profile`;

        const components = [
            new ContainerBuilder()
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent("## Roblox Verification")
                )
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent(
                        `You already have **[${username}](${profileLink})** linked.\n\nTo switch to a different account, click **Change Account**.\nTo continue using this account, click **Continue**.`
                    )
                )
                .addSeparatorComponents(
                    new SeparatorBuilder()
                        .setDivider(true)
                        .setSpacing(SeparatorSpacingSize.Small)
                )
                .addActionRowComponents(
                    new ActionRowBuilder().addComponents(
                        new ButtonBuilder()
                            .setLabel("Change Account")
                            .setEmoji("<:refresh:1456476619580375041>")
                            .setStyle(ButtonStyle.Link)
                            .setURL("https://docksys.xyz/account"),
                        new ButtonBuilder()
                            .setCustomId("continue-verify")
                            .setLabel("Continue")
                            .setEmoji("<:rightarrow:1456476621157433355>")
                            .setStyle(ButtonStyle.Success)
                    )
                ),
        ];

        await interaction.reply({
            flags: MessageFlags.IsComponentsV2,
            ephemeral: true,
            components: components,
        });
    },
};

Button Handler — continueVerify.js

Show Full File
const {
    ContainerBuilder,
    TextDisplayBuilder,
    MessageFlags,
    EmbedBuilder,
    ButtonBuilder,
    ButtonStyle,
    ActionRowBuilder
} = require("discord.js");
const { getRobloxInfo } = require("../Utils/docksystem");

const ADD_ROLE_ID = "1456077337534791701";    // Verified role
const REMOVE_ROLE_ID = "1456077351199838282"; // Unverified/gate role
const LOG_CHANNEL_ID = "1456770628156653850"; // Where to send logs

module.exports = {
    customID: "continue-verify",

    async execute(interaction, client) {
        if (!interaction.deferred && !interaction.replied) {
            await interaction.deferUpdate();
        }

        const { robloxId, username, error } = await getRobloxInfo(
            interaction.user.id,
            interaction,
            client
        );

        if (error) {
            return interaction.editReply({
                content: "Verification failed.",
                components: [],
            });
        }

        const member = await interaction.guild.members.fetch(interaction.user.id);

        // Add verified role, remove unverified role
        await member.roles.add(ADD_ROLE_ID).catch(() => { });
        await member.roles.remove(REMOVE_ROLE_ID).catch(() => { });

        const profileLink = `https://www.roblox.com/users/${robloxId}/profile`;

        const components = [
            new ContainerBuilder()
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent("### Verification Successful")
                )
                .addTextDisplayComponents(
                    new TextDisplayBuilder().setContent(
                        `Verified under **[${username}](${profileLink})**`
                    )
                ),
        ];

        await interaction.editReply({
            flags: MessageFlags.IsComponentsV2,
            components,
        });

        // Build and send log embed
        const logEmbed = new EmbedBuilder()
            .setTitle("New Account Verification")
            .setDescription(
                `**${interaction.user.username}** has **successfully** linked their Roblox account.`
            )
            .addFields(
                { name: "**Joined**", value: ``, inline: true },
                { name: "**Verified At**", value: ``, inline: true },
                { name: "**Roblox Username**", value: `\`${username}\``, inline: true },
                { name: "**Roblox ID**", value: `\`${robloxId.toString()}\``, inline: true },
                { name: "**Discord User**", value: `\`${interaction.user.id}\``, inline: true },
                { name: "**Discord User**", value: `<@${interaction.user.id}>`, inline: true }
            )
            .setImage('https://media.discordapp.net/attachments/1456184292983177349/1456764244245876918/Footer.png?ex=69598ced&is=69583b6d&hm=94f2070aea13e011c2932507cec5ca3360d4ad6847b2c2d496afe3043704a1a4&=&format=webp&quality=lossless&width=1315&height=71')
            .setColor("#2b2d31");

        const profile = new ButtonBuilder()
            .setLabel("View Profile")
            .setURL(profileLink)
            .setStyle(ButtonStyle.Link);

        const actionRow = new ActionRowBuilder().addComponents(profile);

        const logChannel = interaction.guild.channels.cache.get(LOG_CHANNEL_ID);
        if (logChannel) {
            await logChannel.send({ embeds: [logEmbed], components: [actionRow] });
        }
    },
};

Challenge

← Last Lesson You're all caught up!