Udviklere

API-dokumentation

Med API'et kan din FiveM-server, din Discord-bot eller dit eget script tale med dit forum. Det er et tilvalg til 20 kr./md. oven i abonnementet.

  • Synk jobs fra spillet til grupper: får en spiller politijobbet, får de rangen Politibetjent i gruppen Politi.
  • Hent nye whitelist-ansøgninger til din Discord-bot.
  • Slå medlemmer op med deres Discord id, og vis nye tråde andre steder.
Base-URL
https://forum.vascripts.dk/api/v1
Godkendelse
Bearer-nøgle
Grænse
120 kald/min pr. nøgle

Kom i gang

  1. Tilføj API-adgang

    Gå til Mine servere, vælg dit forum og tilføj API-adgang under Abonnement. Det koster 20 kr./md. og virker med det samme.

  2. Lav en nøgle

    Åbn admin-panelet på dit forum og gå til API. Tryk Ny nøgle, giv den et navn som "FiveM-server", og vælg Kun læse eller Læse og skrive. Nøglen vises kun én gang, så gem den med det samme.

  3. Lav dit første kald

    Prøv nøglen i en terminal. Får du dit forums navn tilbage, virker det.

    Terminal
    export VAFORUM_API_KEY="vaf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    
    curl "https://forum.vascripts.dk/api/v1/forum" \
      -H "Authorization: Bearer $VAFORUM_API_KEY"

Godkendelse

Send nøglen med i hvert kald i headeren Authorization. Nøglen bestemmer, hvilket forum du taler med, så du skal ikke sende forummets navn med.

Header
Authorization: Bearer vaf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Der er to slags nøgler:

  • Kun læse må lave alle GET-kald.
  • Læse og skrive må også sætte rang og fjerne medlemmer fra grupper (PUT og DELETE). Prøver en læse-nøgle at skrive, får du 403 read_only_key.

Giv hver ting sin egen nøgle, fx én til FiveM-serveren og én til botten. Så kan du slette den ene uden at stoppe den anden.

Hold nøglen på serveren

Kald kun API'et fra en server: din FiveM-server, din Discord-bot eller din backend. Aldrig fra en browser, en NUI-side eller et client-script. API'et svarer ikke på kald fra browsere (ingen CORS), og en nøgle i en browser eller en client-fil kan alle se.

Er en nøgle sluppet ud, så slet den under API i admin-panelet og lav en ny.

Svar og fejl

Alle svar er JSON. Én ting kommer i data. En liste kommer i data sammen med page og has_more.

Én ting
{
  "data": {
    "slug": "politi",
    "name": "Politi"
  }
}
En liste
{
  "data": [
    {
      "slug": "politi"
    },
    {
      "slug": "ems"
    }
  ],
  "page": 1,
  "has_more": false
}

Går noget galt, får du en HTTP-status og et error med en kode og en besked på dansk. Brug koden i din kode, og vis beskeden til mennesker.

Fejl
{
  "error": {
    "code": "not_found",
    "message": "Medlemmet findes ikke på forummet."
  }
}
  • 400 invalid_requestKaldet er forkert, fx en ugyldig parameter eller body.
  • 401 unauthorizedNøglen mangler eller er forkert.
  • 402 api_not_enabledForummet har ikke API-adgang, eller det er ikke live.
  • 403 read_only_keyNøglen må kun læse.
  • 404 not_foundDet findes ikke.
  • 429 rate_limitedFor mange kald. Headeren Retry-After siger, hvor mange sekunder du skal vente.
  • 500 server_errorNoget gik galt hos os.

Sider

Lister kommer i sider. Er has_more true, er der flere, og du henter næste side med page.

Parametre

pagetal
Siden, du vil have. Starter ved 1.
limit1–100
Hvor mange pr. side. Standard er 50.
Side 2 med 100 pr. side
curl "https://forum.vascripts.dk/api/v1/members?page=2&limit=100" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"

Grænse og log

Hver nøgle må lave 120 kald i minuttet. Går du over, får du 429 rate_limited og en Retry-After-header med det antal sekunder, du skal vente. Kald ikke API'et i et loop hvert sekund. Kald det, når noget sker, fx når en spiller skifter job.

Alle kald bliver logget, undtagen dem der bliver afvist med 429. Admins kan se forbruget og de seneste kald under API i admin-panelet.

Endpoints

Alle stier starter med https://forum.vascripts.dk/api/v1. Hvor der står {member}, kan du bruge medlemmets id eller deres Discord id (kun tal). En spiller findes først på forummet, når de har logget ind der med Discord én gang. Indtil da svarer API'et 404 not_found.

Forum

Hent forummet

GET/forum

Forummet, som nøglen hører til. Godt til at teste, at nøglen virker.

Eksempel
curl "https://forum.vascripts.dk/api/v1/forum" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": {
    "id": "0d6b4f2e-91a3-4c57-b8e0-5a7c3e1f9b62",
    "slug": "nordicrp",
    "name": "Nordic RP",
    "url": "https://nordicrp.forum.vascripts.dk"
  }
}

Medlemmer

Hent medlemmer

GET/members

Forummets medlemmer med rolle og grupper. Find én spiller med deres Discord id, eller søg på navn.

Parametre

discord_idtal
Kun medlemmet med dette Discord id.
searchtekst
Søg i navnet.
pagetal
Siden, du vil have. Starter ved 1.
limit1–100
Hvor mange pr. side. Standard er 50.

role er member, moderator, admin eller owner. groups viser hver gruppe, medlemmet er i, med deres rang.

Eksempel
curl "https://forum.vascripts.dk/api/v1/members?discord_id=284917365029814272" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": [
    {
      "id": "6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
      "name": "Mads Holm",
      "discord_id": "284917365029814272",
      "role": "member",
      "banned": false,
      "joined_at": "2026-09-14T18:22:05.000Z",
      "profile_url": "https://nordicrp.forum.vascripts.dk/u/6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
      "groups": [
        {
          "slug": "politi",
          "name": "Politi",
          "rank": {
            "id": 10,
            "name": "Politibetjent",
            "is_leadership": false
          }
        }
      ]
    }
  ],
  "page": 1,
  "has_more": false
}

Hent ét medlem

GET/members/{member}

Ét medlem, slået op med id eller Discord id.

Eksempel
curl "https://forum.vascripts.dk/api/v1/members/284917365029814272" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": {
    "id": "6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
    "name": "Mads Holm",
    "discord_id": "284917365029814272",
    "role": "member",
    "banned": false,
    "joined_at": "2026-09-14T18:22:05.000Z",
    "profile_url": "https://nordicrp.forum.vascripts.dk/u/6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
    "groups": [
      {
        "slug": "politi",
        "name": "Politi",
        "rank": {
          "id": 10,
          "name": "Politibetjent",
          "is_leadership": false
        }
      }
    ]
  }
}

Grupper

Hent grupper

GET/groups

Alle forummets grupper, også de skjulte (is_public: false). ranks står med den højeste rang først, og slug er det, du bruger i stierne.

Parametre

pagetal
Siden, du vil have. Starter ved 1.
limit1–100
Hvor mange pr. side. Standard er 50.
Eksempel
curl "https://forum.vascripts.dk/api/v1/groups" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": [
    {
      "slug": "politi",
      "name": "Politi",
      "description": "Rigspolitiet i Los Santos.",
      "color": "#2b6cb0",
      "is_public": true,
      "member_count": 14,
      "ranks": [
        {
          "id": 12,
          "name": "Politiinspektør",
          "position": 3,
          "is_leadership": true
        },
        {
          "id": 11,
          "name": "Overbetjent",
          "position": 2,
          "is_leadership": true
        },
        {
          "id": 10,
          "name": "Politibetjent",
          "position": 1,
          "is_leadership": false
        },
        {
          "id": 9,
          "name": "Kadet",
          "position": 0,
          "is_leadership": false
        }
      ]
    },
    {
      "slug": "ems",
      "name": "EMS",
      "description": "Ambulance og hospital.",
      "color": "#c53030",
      "is_public": true,
      "member_count": 9,
      "ranks": [
        {
          "id": 15,
          "name": "Overlæge",
          "position": 2,
          "is_leadership": true
        },
        {
          "id": 14,
          "name": "Paramediciner",
          "position": 1,
          "is_leadership": false
        },
        {
          "id": 13,
          "name": "Ambulanceredder",
          "position": 0,
          "is_leadership": false
        }
      ]
    }
  ],
  "page": 1,
  "has_more": false
}

Hent én gruppe

GET/groups/{slug}

Gruppen med dens ranger og alle medlemmer. since er, hvornår medlemmet kom i gruppen.

Eksempel
curl "https://forum.vascripts.dk/api/v1/groups/politi" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": {
    "slug": "politi",
    "name": "Politi",
    "description": "Rigspolitiet i Los Santos.",
    "color": "#2b6cb0",
    "is_public": true,
    "member_count": 14,
    "ranks": [
      {
        "id": 12,
        "name": "Politiinspektør",
        "position": 3,
        "is_leadership": true
      },
      {
        "id": 11,
        "name": "Overbetjent",
        "position": 2,
        "is_leadership": true
      },
      {
        "id": 10,
        "name": "Politibetjent",
        "position": 1,
        "is_leadership": false
      },
      {
        "id": 9,
        "name": "Kadet",
        "position": 0,
        "is_leadership": false
      }
    ],
    "members": [
      {
        "member": {
          "id": "6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
          "name": "Mads Holm",
          "discord_id": "284917365029814272"
        },
        "rank": {
          "id": 10,
          "name": "Politibetjent"
        },
        "since": "2026-10-02T19:40:00.000Z"
      }
    ]
  }
}

Ansøgninger

Hent ansøgninger

GET/applications

Ansøgninger fra alle forummets skemaer, de nyeste først.

Parametre

statustekst
Kun ansøgninger med status pending, accepted, denied eller withdrawn.
formslug
Kun ét skema, fx whitelist.
pagetal
Siden, du vil have. Starter ved 1.
limit1–100
Hvor mange pr. side. Standard er 50.

answers er spørgsmålene og svarene i skemaets rækkefølge. decided_at og decision_message er null, indtil staff har svaret.

Eksempel
curl "https://forum.vascripts.dk/api/v1/applications?status=pending&form=whitelist" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": [
    {
      "id": 318,
      "form": {
        "slug": "whitelist",
        "name": "Whitelist"
      },
      "status": "pending",
      "applicant": {
        "id": "b3e81d47-0c2f-4a6e-8d19-7f5a2c6e9d03",
        "name": "Rosa Delgado",
        "discord_id": "731205948816302080"
      },
      "answers": [
        {
          "label": "Hvor gammel er du?",
          "value": "24"
        },
        {
          "label": "Hvor længe har du spillet RP?",
          "value": "3 år, mest på danske servere."
        },
        {
          "label": "Fortæl om din karakter",
          "value": "Rosa er natsygeplejerske på Pillbox og har gæld til de forkerte folk."
        }
      ],
      "created_at": "2026-10-29T21:04:11.000Z",
      "decided_at": null,
      "decision_message": null
    }
  ],
  "page": 1,
  "has_more": false
}

Hent én ansøgning

GET/applications/{id}

Én ansøgning med alle svar.

Eksempel
curl "https://forum.vascripts.dk/api/v1/applications/318" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": {
    "id": 318,
    "form": {
      "slug": "whitelist",
      "name": "Whitelist"
    },
    "status": "pending",
    "applicant": {
      "id": "b3e81d47-0c2f-4a6e-8d19-7f5a2c6e9d03",
      "name": "Rosa Delgado",
      "discord_id": "731205948816302080"
    },
    "answers": [
      {
        "label": "Hvor gammel er du?",
        "value": "24"
      },
      {
        "label": "Hvor længe har du spillet RP?",
        "value": "3 år, mest på danske servere."
      },
      {
        "label": "Fortæl om din karakter",
        "value": "Rosa er natsygeplejerske på Pillbox og har gæld til de forkerte folk."
      }
    ],
    "created_at": "2026-10-29T21:04:11.000Z",
    "decided_at": null,
    "decision_message": null
  }
}

Tråde

Hent nye tråde

GET/threads

De nyeste tråde fra boards, som alle kan se, også gæster. Tråde fra staff-boards og gruppe-boards kommer aldrig med. Godt til fx at vise nyheder på en Discord-kanal.

Parametre

boardslug
Kun ét board, fx nyheder.
pagetal
Siden, du vil have. Starter ved 1.
limit1–100
Hvor mange pr. side. Standard er 50.
Eksempel
curl "https://forum.vascripts.dk/api/v1/threads?board=nyheder&limit=5" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": [
    {
      "id": 1482,
      "title": "Ny opdatering: politistationen i Paleto er åben",
      "board": {
        "slug": "nyheder",
        "name": "Nyheder"
      },
      "author": {
        "id": "e2a4c6f8-1b3d-4f5a-9c7e-0d2b4f6a8c1e",
        "name": "Jonas Kjær"
      },
      "reply_count": 23,
      "is_pinned": true,
      "created_at": "2026-10-28T17:00:00.000Z",
      "last_post_at": "2026-10-30T09:12:44.000Z",
      "url": "https://nordicrp.forum.vascripts.dk/t/1482"
    }
  ],
  "page": 1,
  "has_more": true
}

Sæt rang og fjern fra gruppe

Kræver en nøgle med Læse og skrive. Ændringer fra API'et står i forummets audit-log med nøglens navn. De normale regler gælder: medlemmet skal være på forummet, og bannede medlemmer kan ikke komme i en gruppe.

Sæt rang

PUT/groups/{slug}/members/{member}Læse og skrive

Sætter medlemmet i gruppen med den rang. Er de allerede i gruppen, bliver de flyttet til rangen. Du kan trygt sende det samme kald flere gange.

Body (JSON)

ranktekst
Rangens navn (store og små bogstaver er ligegyldige) eller dens id, fx "Politibetjent".

Fejl, du kan få:

  • 400 invalid_request, hvis rank mangler i body, eller hvis personen er bannet fra forummet.
  • 404 not_found, hvis medlemmet ikke er på forummet, eller gruppen eller rangen ikke findes. Ved en ukendt rang står gruppens ranger i beskeden.
Eksempel
curl -X PUT "https://forum.vascripts.dk/api/v1/groups/politi/members/284917365029814272" \
  -H "Authorization: Bearer $VAFORUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rank": "Politibetjent" }'
Svar
{
  "data": {
    "member": {
      "id": "6f1c2a9e-4b7d-4e2a-9c51-3d8e0f7a1b24",
      "name": "Mads Holm",
      "discord_id": "284917365029814272"
    },
    "rank": {
      "id": 10,
      "name": "Politibetjent"
    }
  }
}

Fjern fra gruppe

DELETE/groups/{slug}/members/{member}Læse og skrive

Fjerner medlemmet fra gruppen.

Eksempel
curl -X DELETE "https://forum.vascripts.dk/api/v1/groups/politi/members/284917365029814272" \
  -H "Authorization: Bearer $VAFORUM_API_KEY"
Svar
{
  "data": {
    "removed": true
  }
}

FiveM: synk jobs til grupper

Dette script giver spilleren den rigtige gruppe og rang på forummet, hver gang deres job skifter i spillet. Det virker med både ESX og QBCore. Det kører kun på serveren.

  1. Læg nøglen i server.cfg

    Lav en nøgle med Læse og skrive, og sæt den med set. Brug aldrig setr: den sender værdien til alle spilleres klienter, og så kan de se din nøgle.

    server.cfg
    # server.cfg – brug "set", ikke "setr". Så bliver nøglen på serveren.
    set vaforum_api_key "vaf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ensure vaforum-sync
  2. Lav resourcen

    Lav en mappe vaforum-sync i resources med de to filer herunder. Ret tabellen JOBS, så jobnavne og grades passer til din server, og så gruppernes slug og rangenes navne passer til forummet. Du finder dem med GET /groups.

    vaforum-sync/fxmanifest.lua
    fx_version 'cerulean'
    game 'gta5'
    
    -- Kun server_script: nøglen må aldrig ligge i et client-script.
    server_script 'server.lua'
    vaforum-sync/server.lua
    -- server.lua: synker jobbet i spillet til en gruppe og rang på forummet.
    local API_URL = "https://forum.vascripts.dk/api/v1"
    local API_KEY = GetConvar("vaforum_api_key", "")
    
    -- Job og grade i spillet -> gruppe (slug) og rang (navn) på forummet.
    local JOBS = {
      police = {
        group = "politi",
        ranks = { [0] = "Kadet", [1] = "Politibetjent", [2] = "Overbetjent", [3] = "Politiinspektør" },
      },
      ambulance = {
        group = "ems",
        ranks = { [0] = "Ambulanceredder", [1] = "Paramediciner", [2] = "Overlæge" },
      },
    }
    
    if API_KEY == "" then
      print("[vaforum] Mangler vaforum_api_key i server.cfg")
    end
    
    -- Spillerens Discord id fra identifiers, fx "discord:284917365029814272".
    local function getDiscordId(src)
      for _, identifier in ipairs(GetPlayerIdentifiers(src)) do
        if identifier:sub(1, 8) == "discord:" then
          return identifier:sub(9)
        end
      end
      return nil
    end
    
    local function api(method, path, body)
      PerformHttpRequest(API_URL .. path, function(status, response)
        if status < 200 or status >= 300 then
          print(("[vaforum] %s %s svarede %d: %s"):format(method, path, status, response or ""))
        end
      end, method, body and json.encode(body) or "", {
        ["Authorization"] = "Bearer " .. API_KEY,
        ["Content-Type"] = "application/json",
      })
    end
    
    local function syncJob(src, jobName, grade, oldJobName)
      if API_KEY == "" then return end
      local discordId = getDiscordId(src)
      if not discordId then return end
    
      -- Skiftet job: fjern spilleren fra den gamle gruppe.
      local old = JOBS[oldJobName]
      if old and oldJobName ~= jobName then
        api("DELETE", ("/groups/%s/members/%s"):format(old.group, discordId))
      end
    
      -- Giv den rang, der passer til det nye job og grade.
      local new = JOBS[jobName]
      local rank = new and new.ranks[grade]
      if rank then
        api("PUT", ("/groups/%s/members/%s"):format(new.group, discordId), { rank = rank })
      end
    end
    
    -- ESX
    AddEventHandler('esx:setJob', function(source, job, lastJob)
      syncJob(source, job.name, job.grade, lastJob and lastJob.name)
    end)
    
    -- QBCore sender ikke det gamle job med, så vi husker det selv.
    local lastJobs = {}
    
    AddEventHandler('QBCore:Server:PlayerLoaded', function(Player)
      lastJobs[Player.PlayerData.source] = Player.PlayerData.job.name
    end)
    
    AddEventHandler('QBCore:Server:OnJobUpdate', function(source, job)
      syncJob(source, job.name, job.grade.level, lastJobs[source])
      lastJobs[source] = job.name
    end)
    
    AddEventHandler('playerDropped', function()
      lastJobs[source] = nil
    end)

Spilleren skal have logget ind på forummet med Discord én gang, og Discord skal køre, når de joiner serveren, ellers har de ingen discord:-identifier. Mangler en af delene, sker der ikke noget, og fejlen står i serverkonsollen.

esx:setJob og QBCore:Server:OnJobUpdate kører kun, når jobbet skifter. Vil du også synke, når en spiller joiner, kan du kalde syncJob fra esx:playerLoaded eller QBCore:Server:PlayerLoaded.

Discord-bot: nye ansøgninger

Et lille Node.js-script, der hvert minut henter ventende ansøgninger og skriver de nye i en Discord-kanal via en webhook. En nøgle med Kun læse er nok. Bruger du discord.js, kan du sende til kanalen med channel.send i stedet.

bot.js
// Kør på din bot-server med Node.js 18 eller nyere. Aldrig i en browser.
const API_URL = "https://forum.vascripts.dk/api/v1";
const API_KEY = process.env.VAFORUM_API_KEY;
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; // Kanalindstillinger -> Integrationer -> Webhooks

async function api(path) {
  const res = await fetch(API_URL + path, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  if (res.status === 429) {
    const seconds = Number(res.headers.get("Retry-After") ?? 60);
    throw new Error(`For mange kald. Prøv igen om ${seconds} sekunder.`);
  }
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}

// Alle ventende ansøgninger, side for side.
async function pendingApplications() {
  const all = [];
  for (let page = 1; ; page++) {
    const res = await api(`/applications?status=pending&limit=100&page=${page}`);
    all.push(...res.data);
    if (!res.has_more) return all;
  }
}

const seen = new Set();

async function check() {
  for (const app of await pendingApplications()) {
    if (seen.has(app.id)) continue;
    seen.add(app.id);
    await fetch(WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        content: `Ny ${app.form.name}-ansøgning fra <@${app.applicant.discord_id}> (${app.applicant.name})`,
      }),
    });
  }
}

// Ét kald i minuttet er rigeligt, og langt under grænsen.
check().catch(console.error);
setInterval(() => check().catch(console.error), 60_000);

Scriptet husker kun, hvad det har set, mens det kører. Starter du det igen, sender det de ventende ansøgninger én gang til.