Part 1: Google Workspace Data Ingestion
Set up a Google Cloud service account with domain-wide delegation so ConfigView can pull your Google Workspace data into the dashboard. ConfigView reads Workspace through the Admin SDK and a few related Admin APIs. A single service account, authorized once in your Workspace Admin Console, impersonates an admin and reads users, groups, org units, roles, devices, audit logs, and mailbox settings — all read-only. You’ll end up with two things in ConfigView: a secretGOOGLE_ADMIN_EMAIL (the admin to impersonate) and the service-account key file, uploaded as google_admin.json. The key is not a Secret Manager secret — like Google Cloud, it’s uploaded as a JSON file and referenced directly by the scripts.
Running more than one Google Workspace? Each instance gets its own JSON file and admin-email secret, suffixed by number: the first instance usesgoogle_admin.json+GOOGLE_ADMIN_EMAIL, the second usesgoogle_admin_2.json+GOOGLE_ADMIN_EMAIL_2, and so on. The steps below cover the first instance; for a second instance, just append_2to the filename and the secret name.
Step 1: Create a Google Cloud project
The service account lives in a Google Cloud project. Any project works; API calls are quoted against it.- Go to the Google Cloud Console.
- Click the project dropdown (top-left) → New Project.
- Name it
configview-integration, choose your Organization, and click Create. - Make sure the new project is selected in the dropdown before continuing.
Step 2: Enable the required APIs
On that project, enable the APIs ConfigView calls. On a fresh project these are off by default. Click each link (with your project selected) and press Enable, or run the one-liner below.Note. The Admin SDK exposes both the Directory (directory_v1) and Reports (reports_v1) APIs under the singleadmin.googleapis.comservice — enabling Admin SDK API covers both. You only need the others for the specific endpoints listed above; skip an API and just leave its scripts disabled.
Propagation. Right after enabling, you may see intermittent SERVICE_DISABLED 403s for ~10–15 minutes while the change rolls out. Wait it out before assuming a misconfiguration.
Step 3: Create the service account
- Open IAM & Admin → Service Accounts with your
configview-integrationproject selected. - Click Create Service Account.
- Name:
configview(the email auto-fills, e.g.configview@configview-integration.iam.gserviceaccount.com). - Click Create and Continue.
- Skip the optional “Grant this service account access to project” and “Grant users access” steps — this integration needs no project IAM roles; all access comes from domain-wide delegation in Step 5.
- Click Done.
Step 4: Create and download the JSON key
- From the service account list, click your
configviewservice account. - Open the Keys tab → Add Key → Create new key → JSON → Create.
- The JSON key file downloads automatically. Keep it handy — you’ll upload it to ConfigView in Step 6.
- While you’re here, open the Details tab and copy the Unique ID (the numeric Client ID) — you’ll need it for delegation in Step 5.
Step 5: Authorize domain-wide delegation
This is what lets the service account read your Workspace data. You authorize its Client ID against a fixed list of read-only scopes in the Admin Console.- Sign in to the Google Workspace Admin Console as a super admin.
- Go to Security → Access and data control → API controls → Manage Domain Wide Delegation.
- Click Add new and paste:
- Client ID: the numeric Unique ID from Step 4.4
- OAuth scopes: the comma-separated list below
- Click Authorize.
Seat utilization usesapps.licensing+admin.reports.usage.readonly.apps.licensinganswers “how many seats are assigned” (one row per user per SKU). The purchased count comes from the Reports API customer usage report underadmin.reports.usage.readonly, which exposesaccounts:<edition>_total_licensesalongside_used_licenses.apps.orderdoes not work for this and you can skip it. The Reseller API has the same numbers, but it authenticates the reseller — a customer super admin gets a flat403 Forbiddenon every method even with the scope granted, and buying through a reseller does not change that. It is listed above only because the scope name suggests otherwise. Noteadmin.reports.usage.readonlyis a separate scope fromadmin.reports.audit.readonly— granting the audit one does not grant this one, and vice versa.
Adding scopes later. If you enable a new script that needs a scope you didn’t authorize, re-open Manage Domain Wide Delegation, click your client row, and add the missing scope. Delegation changes take a minute or two to propagate.
Trim if you like. You only need the scopes for the scripts you’ll actually run. The list above is the full set; dropping a scope just disables its endpoints (and its health check).
Step 6: Add the secret and upload the JSON key
6a. Add the admin-email secret.- Go to your ConfigView dashboard:
https://{companyname}.configview.com/admin/integrations/google-workspace - Under Credentials, enter each value and click the save icon on its row:
GOOGLE_ADMIN_EMAIL: a super-admin email address for the service account to impersonate (e.g.admin@yourdomain.com)
- Go to the key file upload page:
https://{companyname}.configview.com/admin/integrations/external(Key files on the Integrations page) - Rename the key file you downloaded in Step 4 to
google_admin.jsonbefore uploading — the scripts resolve it by that exact name. - Upload it. It’s saved to
<project_root>/json/google_admin.jsonon the satellite (the same directory that already holdssecret-manager.json). It is not a Secret Manager secret — the key is referenced directly by all Google Workspace scripts.
Second instance. For a second Google Workspace, name the secretGOOGLE_ADMIN_EMAIL_2and upload the key asgoogle_admin_2.json(instance 3 →_3, and so on).
Step 7: Enable the Google Workspace app in ConfigView
- Go to:
https://{companyname}.configview.com/admin/integrations/google-workspace - Click Connect. ConfigView creates its tables and schedules every collector at your default run time.
- Click Verify now to confirm the credentials work.
Step 8: Verify
- Go to:
https://{companyname}.configview.com/admin/status/ - Run the Google Workspace health check.
- All checks should pass.
unauthorized_client/Client is unauthorized to retrieve access tokens— the Client ID isn’t authorized, or a required scope is missing. Re-do Step 5 and confirm the Unique ID matches. Note this failure happens when the token is issued, before any API call, so a script hitting it produces no 403 and no error rows — its table simply stays empty. Scopes are authorized individually: the rest of the integration can be working perfectly while one un-granted scope leaves its own endpoints silently at zero. If one table is empty and the others are fine, suspect its scope first.access_denied/403on a specific endpoint — the matching scope wasn’t authorized, orGOOGLE_ADMIN_EMAILisn’t a super admin.SERVICE_DISABLED— the API for that endpoint isn’t enabled on the project. Re-check Step 2.FileNotFoundError/google_admin.json— the key file wasn’t uploaded, or was uploaded under the wrong name. Re-do Step 6b and confirm it’s named exactlygoogle_admin.json(orgoogle_admin_2.jsonfor a second instance).- Otherwise verify your
GOOGLE_ADMIN_EMAILsecret and the uploaded JSON key are both in place.
Data Tables
Once the scripts run, the corresponding Google Workspace tables will be created in your database. All tables include arun_at column for historical tracking.
The License Assignments script (google_get_licenses → google_licenses) records one row per user per SKU across Google Workspace, Gemini, Vault, Voice and AppSheet, so you can see how many seats of each SKU are actually assigned. It stores product_id and sku_id verbatim — the friendly product_name / sku_name come from a static catalog and are NULL for any SKU Google has added since, so filter and join on the ids, never the names. This is the assigned side.
The License Totals script (google_get_license_totals → google_license_totals) supplies the purchased side, from the Reports API customer usage report: one row per edition per day with total_licenses and used_licenses. Together they answer “are we at our seat limit” — the curated query google.license_utilization renders it directly as purchased / used / available / percent.
Two things to know about it. The report is compiled daily and lags two to three days, so report_date is never today; asking for a newer date returns a 400 naming the newest one that exists, which the collector follows automatically. And its edition values are Google’s legacy gsuite_* names, which do not match the modern SKU names in google_licenses — compare the two tables’ numbers, don’t join them on the label.
The Gemini Activity script (google_get_gemini_activity → google_gemini_activity_daily) reports on Gemini use across Gmail, Drive, Docs, Calendar, Sheets, Meet and Workflows, from the Reports API gemini_in_workspace_apps feed.
It stores a daily rollup, not raw events — one row per person per day per app, surface, action and outcome, with an event count. That matters because Gemini generates an event every time it offers itself, not only when someone uses it: on a typical tenant more than 99% of events are event_category = 'inactive' (a proofread underline nobody accepted, a scheduling chip nobody clicked). Storing every one of those was 7 GB for a fortnight of history on a 150-seat tenant; the rollup holds the same analysis in single-digit MB per year and keeps history indefinitely.
Two consequences to know about:
- It is a ledger, not a snapshot. Every other Google table here keeps one snapshot and you filter it with
run_at = (SELECT MAX(run_at) …). This table keeps every date it has ever collected — the collector rewrites only the last five days on each run. Filtering it onMAX(run_at)would throw away all but the most recent refresh. Scope it byactivity_dateinstead. - Split active from passive. Any query that counts rows without excluding
event_category = 'inactive'is measuring how often Gemini appeared, not how often it was used. The catalog ships 28 curated queries that already make this distinction — seat waste, take-up rate per app, real user list, daily active users, collection completeness — under thegoogle.gemini_*andlookup.users_with_gemini_*keys, plus two per-user cards on the User page.
google-workspace-2), it collects the same rollup into google_gemini_activity_daily_2 and ships the same questions under google2.gemini_*. One difference to know: the second-tenant app has no licence collector, so the seat side of those queries counts active directory accounts rather than purchased seats and is labelled as a bound rather than a figure.
Gemini is bundled into the Business and Enterprise edition price rather than sold as its own SKU, so there is usually no Gemini line item to reclaim. google.gemini_billing_sku_check confirms which case you are in, and google.gemini_cost_of_unused_seats takes the per-seat uplift as a parameter because no API reports it.
The Group Email Activity script (google_get_groups_email_activity → google_groups_email_daily + google_groups_email_coverage) answers the question nothing else in Google’s APIs can: which groups does nobody email any more? The Directory and Groups Settings APIs describe a group but never record when it was last used, so this script reads the Gmail log in the Reports API and keeps one row per group per day it received mail. Messages are counted once each, however many members they reached, and spam is counted separately so a group that only receives spam still shows as stale.
It needs no extra scope (the audit scope above covers it) and works on Business Plus as well as Enterprise. Three things to know:
- History fills in over several runs. Google keeps about six months of Gmail log. A small tenant loads all of it on the first run; a busy one (~100k mail events a day) loads about a month per run, newest first, and finishes within a week.
google_groups_email_coveragerecords every day that has been read to the last page. - Stale only means stale over a covered window.
google.groups_not_emailedshows ahistorycolumn. Until it readscomplete, a group on that list may simply not have been checked yet.google.groups_email_collection_completenessshows which days are still missing. - It is a ledger, like the Gemini rollup. Scope it by
activity_date, never byMAX(run_at).
_2 appended and ships the same three queries under google2.*.
The Admin Audit Log script (google_admin_audit) pulls the Google Admin Console activity feed from the Reports API — user, group, role, and security-setting changes over a rolling 180-day window. Curated queries built on it (recent admin actions, role & privilege changes, security-setting changes, and per-user investigation lookups) ship in the saved-query catalog under the Audit category.