Google Forms Setup Guide

Complete Walkthrough for Google Forms API & Service Account Cloud Synchronization

Finals Management features a native, automated Google Forms API integration powered by server/google_auth.js. This allows FiveM servers to accept job applications through standard web-based Google Forms and automatically synchronize submissions directly into candidate dossiers within the in-game command tablet.


⚙️ How It Works Under The Hood

  • Automated OAuth2 Token Manager (server/google_auth.js): When your server boots, google_auth.js reads your service_account.json, signs a cryptographically secure RS256 JWT assertion, and requests a scoped Bearer token from https://oauth2.googleapis.com/token.
  • API Scopes Used:
    • https://www.googleapis.com/auth/forms.responses.readonly
    • https://www.googleapis.com/auth/forms.body.readonly
  • Zero-Touch Token Renewal: Tokens are cached in server memory and automatically renewed every 45 minutes in the background. No manual token regeneration or server restarts required.
  • Dynamic Question Schema Fetching: The script queries the Google Forms metadata API to dynamically pull your actual question titles and maps them cleanly into applicant answers.
  • Smart Keyword Mapping: Answers for questions containing name or applicant are mapped to the applicant name; questions containing citizen, cid, or state id are mapped to the player's Citizen ID.

📋 Step-by-Step Setup Guide


Step 1: Create a Google Cloud Project & Enable the Forms API

  1. Navigate to the Google Cloud Console.
  2. Log in with your Google account.
  3. In the top navigation bar, click the project dropdown and select New Project.
  4. Name your project (e.g., FiveM-Department-Recruitment) and click Create.
  5. Ensure your newly created project is selected in the top bar.
  6. Open the left sidebar menu (☰) and navigate to APIs & Services > Library.
  7. In the search box, search for Google Forms API.
  8. Click on Google Forms API and click the blue Enable button.

Step 2: Create a Service Account & Download service_account.json

  1. In the left navigation menu, go to IAM & Admin > Service Accounts.

  2. Click + Create Service Account at the top.

  3. Enter a Service account name (e.g., finals-management).

  4. Click Create and Continue, then click Done (granting project roles is optional).

  5. In the Service Accounts list, locate the account you just created and click on it.

  6. Navigate to the Keys tab at the top.

  7. Click Add Key > Create new key.

  8. Select JSON as the key type and click Create.

  9. A .json file will immediately download to your computer.

  10. Rename this downloaded file to:

    Plain text
    service_account.json
  11. Move service_account.json directly into your server's resource root folder:

    Plain text
    resources/[finals]/finals_management/service_account.json

[!IMPORTANT] The file name must be exactly service_account.json and placed in the root directory of finals_management alongside fxmanifest.lua and config.lua.


Step 3: CRITICAL — Share Your Google Form with the Service Account

[!WARNING] This is the #1 most common installation mistake! A Google Service Account cannot read private Google Forms by default. You MUST grant the service account permission to view your Google Form.

  1. Open your service_account.json file in a text editor (e.g., VS Code or Notepad).

  2. Locate the line with client_email:

    JSON
    "client_email": "finals-management@your-project-id.iam.gserviceaccount.com"
  3. Copy this entire email address.

  4. Open the Google Form you want to link in your web browser.

  5. In the top-right corner of the Google Form editor, click the Three Dots menu (⋮) next to the purple "Send" button.

  6. Click Add collaborators (or share settings).

  7. Paste the copied service account email address into the "Add people and groups" field.

  8. Ensure the permission is set to Editor (or Viewer).

  9. Uncheck "Notify people" (since the service account has no inbox) and click Send / Share.


Step 4: Design Your Form & Utilize Smart Field Mapping

When civilians submit the form, finals_management automatically maps responses into the candidate dossier using smart keyword detection. Structure your questions as follows:

Field in Tablet DossierRequired Question Title KeywordExample Question Titles in Google Form
Applicant NameContains name, candidate, or applicant"Full Character Name", "Applicant First & Last Name", "Candidate Name"
Citizen ID / CIDContains citizen, cid, state id, or passport"Citizen ID / State ID", "Character CitizenID", "Passport / CID Number"
All Other QuestionsAny custom title"Previous Department Experience", "Weekly Availability", "Why join LSPD?", "Discord Handle"

[!TIP] If an applicant leaves the Citizen ID field blank, the script automatically assigns a temporary reference identifier (GFORM-XXXXXX) so the application can still be reviewed, interviewed, and promoted in-game.


Step 5: Configure config.lua

Open config.lua and configure your department's Google Form URL:

Lua
Config.Application = {
    enabled = true,

    --- Set global mode to 'google_forms'
    mode = 'google_forms', -- 'in_game' | 'google_forms'

    --- Submissions retention (older filings automatically purged from database)
    filingRetentionDays = 7,
    filingRetentionCheckMinutes = 60,

    jobs = {
        police = {
            label = 'Los Santos Police Department',

            --- Official Google Form URL (supports both viewform and edit URLs)
            googleFormUrl = 'https://docs.google.com/forms/d/1rtPTMm7Rs4WgUweuGFaavEwcS3yU0eDVT6AiEstm3_Q/viewform',

            --- In-world recruitment desk NPC
            ped = {
                model = 's_f_y_cop_01',
                coords = { x = 440.6752, y = -978.9439, z = 30.6896, w = 176.7222 },
                scenario = 'WORLD_HUMAN_GUARD_STAND',
            },

            --- Default interview room waypoint set when scheduling interviews
            defaultInterviewLocation = 'Mission Row PD — Interview Room 2',
        },
        ambulance = {
            label = 'Emergency Medical Services',
            googleFormUrl = 'https://docs.google.com/forms/d/YOUR_EMS_FORM_ID/viewform',
            ped = {
                model = 's_m_m_doctor_01',
                coords = { x = -247.1, y = 6331.4, z = 32.4, w = 220.0 },
            },
            defaultInterviewLocation = 'Pillbox Hill Medical Center — Reception',
        },
    },
}

Step 6: In-Game Management & Cloud Synchronization

  1. In-game, open the management tablet using /management (as an authorized supervisor or boss).
  2. Navigate to the Recruitment tab.
  3. In Google Forms mode, the tablet displays:
    • Connected Google Form ID and link.
    • Sync Cloud Responses button.
    • Live list of candidate dossiers pulled directly from Google Forms.
  4. Supervisors can:
    • Inspect Dossier: Read full candidate answers, timestamp, and Citizen ID.
    • Schedule Interview: Sets an in-game GPS waypoint to the department's interview location and notifies the candidate.
    • Approve / Accept: Prepares the digital employment offer contract for signing.
    • Decline: Closes the application dossier and logs the decision to Discord.

Step 7 (Optional): Automated Form Response Deletion via Apps Script

When a supervisor declines or deletes a candidate submission in-game, you can optionally configure Google Forms to automatically delete that response from the Google Form so it does not clutter your spreadsheet.

  1. Open your Google Form in your browser.
  2. Click the Three Dots menu (⋮) in the top right > < > Script editor.
  3. Delete any default code in Code.gs and paste the following snippet:
JavaScript
function doPost(e) {
  try {
    var data = JSON.parse(e.postData.contents);
    if (data.action === "deleteResponse" && data.formId && data.responseId) {
      var form = FormApp.openById(data.formId);
      form.deleteResponse(data.responseId);
      return ContentService.createTextOutput(JSON.stringify({ ok: true }))
        .setMimeType(ContentService.MimeType.JSON);
    }
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: err.message }))
      .setMimeType(ContentService.MimeType.JSON);
  }
  return ContentService.createTextOutput(JSON.stringify({ ok: false }))
    .setMimeType(ContentService.MimeType.JSON);
}
  1. Click Deploy > New deployment.

  2. Select type: Web app.

  3. Set:

    • Execute as: Me (your Google email)
    • Who has access: Anyone
  4. Click Deploy and authorize the script permissions.

  5. Copy the generated Web App URL (e.g., https://script.google.com/macros/s/AKfycb.../exec).

  6. Paste this URL into config.lua:

    Lua
    Config.Application.googleApi = {
        deleteWebhookUrl = 'https://script.google.com/macros/s/YOUR_APPS_SCRIPT_ID/exec',
    }

🛠️ Troubleshooting & FAQs

Error MessageRoot CauseSolution
HTTP 403: The caller does not have permissionService account is not shared on the Google Form.Open your Google Form -> Click Three Dots (⋮) -> Add collaborators -> Paste client_email from service_account.json as Editor.
HTTP 404: Not FoundInvalid Form ID or Form deleted.Verify the URL in config.lua. Ensure you copied the full link from your browser.
Failed to read service_account.jsonFile missing or misplaced.Ensure service_account.json is located in the root of finals_management (next to fxmanifest.lua).
Applicant name appears blankQuestion title keyword mismatch.Rename the question in Google Form to include the word Name (e.g. "Applicant Character Name").
Citizen ID displays as GFORM-XXXXXXForm does not have a Citizen ID question.Add a short answer question titled "Citizen ID" or "State ID" to your Google Form.
Token fails to generate on startupInvalid private key or clock skew.Verify system clock on your server machine is accurate (OAuth JWT rejects requests with skewed timestamps).