Official Documentation

Verify SA-MP

Welcome to the official documentation of Verify SA-MP. Here you will learn how to integrate your SA:MP, MTA:SA, or FiveM server with Discord using a fully automated whitelist system.

  • The player completes the whitelist on Discord
  • Staff approves or denies the request
  • The server checks the API before allowing entry

Administrative Commands

/announcewl

Sends an announcement that the whitelist is open.

/approve

Approve a user on the whitelist.

/deny

Reject a user from the whitelist.

/dashboard

Shows the bot configuration dashboard.

/set-addquestions

Add a multiple-choice whitelist question.

/set-questions

Set the open-answer whitelist questions.

/getkey

Generate or retrieve your API key.

/profile set avatar

Change the bot avatar.

/profile set banner

Change the bot banner.

/profile set bio

Change the bot bio.

General Commands

/help

Shows all available bot commands.

/botinfo

Verify SA-MP — Bot Information

/online

Shows list of online players in an SA:MP server.

/support

Shows the official support server information.

/whitelist

Starts the whitelist process.

How the API works

  1. The player completes the whitelist on Discord
  2. Staff approves or denies
  3. Status is stored in the API
  4. The server periodically queries the API
  5. Only approved players can join

How to obtain the API Key

Use the command below on Discord:

/obterkey
/getkey

Authentication

Authorization: Bearer API_KEY

Endpoint

GET https://verifyservers.discloud.app/api/whitelist/my

API Response

{
  "success": true,
  "count": 3,
  "whitelists": [
    {
      "identifier": "JoaoRP",
      "status": "approved"
    },
    {
      "identifier": "MariaRP",
      "status": "denied"
    },
    {
      "identifier": "Noobzinho",
      "status": "pending"
    }
  ]
}

SA:MP Integration (Pawn)

#include <a_samp>
#include <a_https>
#include <DOF2>

#define API_URL "https://verifyservers.discloud.app/api/whitelist/my"
#define API_KEY "YOUR_API_KEY"    // <-- change here
#define WL_DIR "/whitelistsverify" // Caminho desejado
new WL_DirAvailable = 0; // Indica se o diretório existe (avaliado em OnGameModeInit)

#if defined FILTERSCRIPT

public OnFilterScriptInit()
{
	print("
--------------------------------------");
	print(" Blank Filterscript by your name here");
	print("--------------------------------------
");
	return 1;
}

public OnFilterScriptExit()
{
	return 1;
}

#else

main()
{
	print("
----------------------------------");
	print(" === TESTE: O GM INICIOU");
	print("----------------------------------
");
}

#endif

public OnGameModeInit()
{
	// Don't use these lines if it's a filterscript
	SetGameModeText("Blank Script");
	AddPlayerClass(0, 1958.3783, 1343.1572, 15.3746, 269.1425, 0, 0, 0, 0, 0, 0);

 	SetTimer("ProcessHTTPSQueue", 100, true);
    SetTimer("CheckWhitelistTimer", 15000, true);

    // checa diretório e marca WL_DirAvailable
    WL_DirAvailable = EnsureWhitelistDirectory();

    if (!WL_DirAvailable)
    {
        printf("[WHITELIST] Pasta '%s' NÃO encontrada ou inacessível.", WL_DIR);
        printf("[WHITELIST] Crie a pasta manualmente e dê permissão de escrita ao usuário do servidor.");
        printf("[WHITELIST] No Windows (cmd):  mkdir "%s"", WL_DIR);
        printf("[WHITELIST] No Linux: mkdir -p "%s" && chown <user> "%s"", WL_DIR, WL_DIR);
        printf("[WHITELIST] Vou usar fallback para a pasta DOF2 padrão (Users/...).");
    }
    else
    {
        printf("[WHITELIST] Pasta '%s' disponível — arquivos serão salvos nela.", WL_DIR);
    }
	return 1;
}

public OnGameModeExit()
{
    DOF2::WriteFile();
    DOF2_Exit();
	return 1;
}


public CheckWhitelistTimer()
{
    https_set_header("Authorization", "Bearer " API_KEY);

    https(0, HTTPS_GET, API_URL, "", "OnWhitelistResponse");
    return 1;
}

public OnWhitelistResponse(index, response[], status, error)
{
    if (error)
    {
        printf("[WHITELIST] Requisição falhou (error=%d, status=%d)", error, status);
        return 1;
    }

    if (status != 200)
    {
        printf("[WHITELIST] Status != 200: %d Body: %s", status, response);
        return 1;
    }

    new body[8192];
    strmid(body, response, 0, sizeof body);
    Trim(body);

    printf("[WHITELIST] Resposta: %s", body);

    new idx = FindSubstring(body, ""whitelists"", 0);
    if (idx == -1)
    {
        printf("[WHITELIST] 'whitelists' não encontrada no JSON.");
        return 1;
    }

    new arrStart = FindSubstring(body, "[", idx);
    if (arrStart == -1)
    {
        printf("[WHITELIST] array '[' não encontrado após 'whitelists'.");
        return 1;
    }

    new arrEnd = FindSubstring(body, "]", arrStart);
    if (arrEnd == -1) arrEnd = strlen(body);

    new pos = arrStart + 1;
    new processed = 0;

    while (pos < arrEnd)
    {
        new objPos = FindSubstring(body, "{", pos);
        if (objPos == -1 || objPos >= arrEnd) break;

        new objEnd = FindSubstring(body, "}", objPos);
        if (objEnd == -1 || objEnd > arrEnd) objEnd = arrEnd;

        new identifier[128]; identifier[0] = '';
        new wl_status[64]; wl_status[0] = '';
        new tempNext;

        if (!GetJsonValueInRange(body, objPos, objEnd, "identifier", identifier, sizeof identifier, tempNext))
        {
            GetJsonValueInRange(body, objPos, objEnd, "id", identifier, sizeof identifier, tempNext);
        }
        GetJsonValueInRange(body, objPos, objEnd, "status", wl_status, sizeof wl_status, tempNext);

        Trim(identifier); Trim(wl_status);

        if (identifier[0])
        {
            // sanitize filename
            new safe[64];
            SanitizeFilename(identifier, safe, sizeof safe);

            // monta caminho alvo: se WL_DirAvailable use scriptfiles/whitelistsverify/<safe>.ini
            new userfile[160];
            if (WL_DirAvailable)
            {
                format(userfile, sizeof userfile, "%s/%s.ini", WL_DIR, safe); // 'scriptfiles/whitelistsverify/safe.ini'
            }
            else
            {
                // fallback para DOF2::File (ex: Users/<safe>.ini)
                format(userfile, sizeof userfile, DOF2::File(safe));
            }

            if (DOF2::FileExists(userfile))
            {
                DOF2::SetString(userfile, "identifier", identifier);
                DOF2::SetString(userfile, "status", wl_status);
                DOF2::WriteFile();
                printf("[WHITELIST] Atualizado: %s -> status=%s (arquivo: %s)", identifier, wl_status[0] ? wl_status : "n/a", userfile);
            }
            else
            {
                if (!DOF2::CreateFile(userfile, "")) // senha vazia
                {
                    printf("[WHITELIST] Falha ao criar arquivo DOF2 para %s (caminho: %s)", identifier, userfile);
                }
                else
                {
                    DOF2::SetString(userfile, "identifier", identifier);
                    DOF2::SetString(userfile, "status", wl_status);
                    DOF2::WriteFile();
                    printf("[WHITELIST] Criado: %s -> status=%s (arquivo: %s)", identifier, wl_status[0] ? wl_status : "n/a", userfile);
                }
            }
            processed++;
        }
        else
        {
            printf("[WHITELIST] item sem identifier encontrado, pulando.");
        }

        pos = objEnd + 1;
    }

    if (!processed) printf("[WHITELIST] Nenhuma whitelist processada nesta chamada.");
    else printf("[WHITELIST] Processadas %d entradas.", processed);

    return 1;
}

/* -----------------------
   Helpers
   ----------------------- */

// tenta verificar a pasta WL_DIR: se for possível criar um arquivo .touch nela significa que existe/permissão
// retorna 1 se ok, 0 se não
stock EnsureWhitelistDirectory()
{
    new testpath[256];
    format(testpath, sizeof testpath, "%s/.touch", WL_DIR);

    // tenta abrir para escrita (io_write). fopen retorna handle>0 se conseguiu.
    new File:f = fopen(testpath, io_write);
    if (f)
    {
        // criou arquivo de teste: fecha e remove
        fclose(f);
        // tenta remover o arquivo
        #if defined __PAWN__
        // tentar fremove (padrão do pawn)
        #endif
        fremove(testpath);
        return 1;
    }
    return 0;
}

// procura substring 'needle' em 'hay' a partir de 'startpos'; retorna índice ou -1
stock FindSubstring(const hay[], const needle[], startpos)
{
    new hlen = strlen(hay);
    new nlen = strlen(needle);
    if (startpos < 0) startpos = 0;
    if (nlen == 0) return startpos;
    if (hlen < nlen || startpos > hlen - nlen) return -1;

    new i, j, ok;
    for (i = startpos; i <= hlen - nlen; i++)
    {
        ok = 1;
        for (j = 0; j < nlen; j++)
        {
            if (hay[i + j] != needle[j]) { ok = 0; break; }
        }
        if (ok) return i;
    }
    return -1;
}

// Extrai "key": "value" ou key: value entre range [rangeStart, rangeEnd).
stock GetJsonValueInRange(const body[], rangeStart, rangeEnd, const key[], out[], outsize, &nextpos)
{
    if (rangeStart < 0) rangeStart = 0;
    if (rangeEnd <= rangeStart) return 0;

    new pattern[128];
    format(pattern, sizeof pattern, ""%s"", key);

    new idx = FindSubstring(body, pattern, rangeStart);
    if (idx == -1 || idx >= rangeEnd) return 0;

    new p = idx + strlen(pattern);
    while (p < rangeEnd && body[p] != ':') p++;
    if (p >= rangeEnd) return 0;
    p++;

    while (p < rangeEnd && (body[p] == ' ' || body[p] == '	' || body[p] == '
' || body[p] == '
')) p++;
    if (p >= rangeEnd) return 0;

    if (body[p] == '"')
    {
        p++;
        new i = 0;
        while (p < rangeEnd)
        {
            new ch = body[p];
            if (ch == '\')
            {
                p++;
                if (p >= rangeEnd) break;
                if (i < outsize - 1) out[i++] = body[p];
                p++;
                continue;
            }
            if (ch == '"') break;
            if (i < outsize - 1) out[i++] = ch;
            p++;
        }
        out[i] = '';
        nextpos = (p < rangeEnd) ? (p + 1) : rangeEnd;
        return 1;
    }
    else
    {
        new i = 0;
        while (p < rangeEnd)
        {
            new ch = body[p];
            if (ch == ',' || ch == '}' || ch == ']' || ch == '
' || ch == '
') break;
            if (i < outsize - 1) out[i++] = ch;
            p++;
        }
        out[i] = '';
        Trim(out);
        nextpos = p;
        return 1;
    }
}

// Remove chars inválidos do filename
stock SanitizeFilename(const src[], out[], outsize)
{
    new i = 0, j = 0;
    new len = strlen(src);
    if (outsize <= 1) return 0;
    for (i = 0; i < len && j < outsize - 1; i++)
    {
        new c = src[i];
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
            c == '-' || c == '_' || c == '.') {
            out[j++] = c;
        } else {
            out[j++] = '_';
        }
    }
    out[j] = '';
    return 1;
}

forward CheckWhitelistTimer();
forward OnWhitelistResponse(index, response[], status, error);

// Trim básico
stock Trim(str[])
{
    new len = strlen(str);
    new s = 0, e = len - 1;
    while (s < len && (str[s] == ' ' || str[s] == '	' || str[s] == '
' || str[s] == '
')) s++;
    while (e >= 0 && (str[e] == ' ' || str[e] == '	' || str[e] == '
' || str[e] == '
')) e--;
    if (s == 0 && e == len - 1) return 1;
    if (e < s) { str[0] = ''; return 1; }
    new tmp[512];
    new j = 0;
    new i;
    for (i = s; i <= e && j < sizeof tmp - 1; i++) tmp[j++] = str[i];
    tmp[j] = '';
    format(str, strlen(str), "%s", tmp);
    return 1;
}

MTA:SA Integration (Lua)

-- Define a URL da API e a chave de autenticação
local API_URL = "https://verifyservers.discloud.app/api/whitelist/my"
local API_KEY = "SUA_API_KEY"
local whitelist = {}

-- Função que atualiza a whitelist
function atualizarWhitelist()
    fetchRemote(API_URL, {
        headers = { ["Authorization"] = "Bearer "..API_KEY }
    }, function(res, info)
        if info.success then
            local data = fromJSON(res)
            whitelist = {}
            -- Armazena os dados da whitelist
            for _, v in ipairs(data.whitelists) do
                whitelist[v.identifier] = v.status
            end
        end
    end)
end

-- Atualiza a whitelist quando o recurso iniciar e periodicamente a cada 5 minutos
addEventHandler("onResourceStart", resourceRoot, function()
    atualizarWhitelist()
    setTimer(atualizarWhitelist, 300000, 0) -- Atualiza a cada 5 minutos
end)

-- Verifica se o jogador está na whitelist ao se conectar
addEventHandler("onPlayerConnect", root, function(name)
    if whitelist[name] ~= "approved" then
        cancelEvent(true, "Você não está aprovado na whitelist.")
    end
end)

FiveM Integration (Lua)

-- Define a URL da API e a chave de autenticação
local API_URL = "https://verifyservers.discloud.app/api/whitelist/my"
local API_KEY = "SUA_API_KEY"
local whitelist = {}

-- Função que atualiza a whitelist
function atualizarWhitelist()
    PerformHttpRequest(API_URL, function(code, res)
        if code == 200 then
            local data = json.decode(res)
            whitelist = {}
            -- Armazena os dados da whitelist
            for _, v in ipairs(data.whitelists) do
                whitelist[v.identifier] = v.status
            end
        end
    end, "GET", "", {
        ["Authorization"] = "Bearer "..API_KEY
    })
end

-- Atualiza a whitelist quando o recurso iniciar e periodicamente a cada 5 minutos
CreateThread(function()
    atualizarWhitelist()
    while true do
        Wait(300000) -- Atualiza a cada 5 minutos
        atualizarWhitelist()
    end
end)

-- Verifica se o jogador está na whitelist ao tentar conectar
AddEventHandler("playerConnecting", function(name, _, deferrals)
    deferrals.defer()
    Wait(0)

    -- Se o jogador não estiver aprovado, bloqueia a conexão
    if whitelist[name] ~= "approved" then
        deferrals.done("Você não está aprovado na whitelist.")
        CancelEvent()
        return
    end

    deferrals.done()
end)