Why teams build this
Security and GRC leaders are increasingly asked to show that the trust center is doing commercial work. The question from leadership is usually some version of “which accounts are reviewing us, and what are they reading?” Answering it in a spreadsheet gets stale within a week. Answering it in Hex lets you join trust center activity to the CRM and pipeline data you already have there, so document engagement sits next to deal stage and account owner.What one row looks like
The dataset this guide produces has one row per interaction:
One request returns the whole trust center, so you do not need to know your document list in advance or loop over it. No cell is ever null. Rows carry
unattributed and an empty email when a visit has not been tied to a company or a person, and two boolean columns let you filter to the level of attribution you want. See how much of the activity is identified.
event_type currently returns one of DOCUMENT_VIEWED, DOCUMENT_DOWNLOADED, or LINK_DOCUMENT_OPENED. Treat it as a passthrough column so new activity types flow into your dashboard without a code change.
Before you start
Bind the key to a service account. A scheduled Hex refresh then keeps working when the person who created the key changes roles or leaves the company. See service accounts.
Step 1: Create an API key
1
Create the service account first
Go to Settings → Service accounts and create one named something like
Hex reporting. Set its role to Admin, because the endpoint this guide uses is admin only and the role selector starts on User.Do this before opening API settings. The Owner dropdown on the key dialog lists the service accounts that already exist, so it offers only Yourself until one is there to pick. Full walkthrough: service accounts.2
Open API settings
Go to Settings → API. This page is visible to admins.
3
Create the key
Click Create API key. Give it a Name such as
Hex trust center reporting, set Owner to your service account, and pick an Expiration.4
Copy it once
The key is shown a single time and starts with
wolfia-api-. Copy it before closing the dialog.Step 2: Store the key as a Hex secret
Open the Variables sidebar in your Hex project. In the Secrets section, click +Add, choose Create project secret, and name itWOLFIA_API_KEY.
Hex stores secrets in an encrypted vault and renders them as [SECRET VALUE] if anything tries to display them, so the key stays out of your notebook source and out of any published app. A secret is referenced in a Python cell by its bare name, which is why the sample below uses WOLFIA_API_KEY directly with no lookup around it.
Step 3: Build the interactions dataframe
One endpoint returns every document interaction across your whole trust center, so this cell is a single loop rather than a walk over each document in turn. Each request returns up to 100 interactions plus anext_cursor. Keep passing that cursor back while has_more is true, and stop when it turns false. A trust center with a few hundred interactions is a handful of requests.
How much of the activity is identified
A trust center is a public page. Someone can read your SOC 2 before they ever tell you who they are, so every interaction falls into one of three levels of identification. The sample code labels all three so your dashboard has no blank cells, and it sets two boolean columns so you can filter deliberately.
Two things follow from how this works, and both are useful:
- An email never appears without a domain. Individual identification comes from an access request, and an access request always belongs to an account. So
account_domainis always at least as complete asuser_email, and it is the right column to group by for account level reporting. - Unidentified visits are real engagement. Dropping them understates how much your trust center is being read, which is usually the opposite of the point. Keep them in the totals and filter only when you specifically need account level attribution:
Choosing your join keys
Usedocument_title as the document key. For a document that is still on your trust center, the title is the one you gave it, read live at the time of the request. Renaming a document therefore updates its whole history at once, and re-uploading a refreshed SOC 2 under the same title keeps that history in one place. document_id changes on a re-upload, which makes it a poor primary key for a saved dashboard even though it is useful for spot checks.
One caveat: grouping is by exact title, so SOC 2 Type 2 Report and Acme SOC 2 Type II Report count as two documents. If you have used both spellings, map the variants to one canonical name in your dashboard.
Check is_deleted before you trust a title. When it is false, the title came from your trust center. When it is true the document id no longer resolves to anything on your trust center, and the title falls back to what the visitor’s browser reported at the time of the click, which may be an older name or missing entirely. Removed documents are a small share of most trust centers, but the flag is what tells you which rows to treat carefully.
Use account_domain as the customer key. It is the stable identifier for a visiting company and joins cleanly to the website or domain field on your CRM accounts.
Keeping the dataset fresh
Re-pull a trailing window on every run
Re-pull a trailing window on every run
A visitor can browse before they finish verifying their identity. When they do verify, their earlier activity is attributed to them and to their account, so a row that was anonymous an hour ago can gain an
account_domain and a user_email.Set each scheduled run to re-pull the last 30 days and let event_id de-duplicate. Rows that gained attribution get corrected, and nothing is double counted.De-duplicate on event_id
De-duplicate on event_id
event_id is unique and stable per interaction. It is the right key for an upsert into a warehouse table and for drop_duplicates in a notebook.Backfill once, then run incrementally
Backfill once, then run incrementally
Run the full historical range once and write it to a table. After that, schedule a run over a trailing window only, using
start_date and end_date rather than a saved cursor.Persist a cursor between runs with order=received
Persist a cursor between runs with order=received
The default ordering follows when interactions happened, and the reported time comes from the visitor’s browser, so a late arriving interaction can land behind a position you already passed. Default cursors therefore live inside one walk: finish each walk in one run.For scheduled incremental runs, pass
order=received instead. It sorts by when Wolfia recorded each interaction, so its cursor is durable: persist it between runs and resume exactly where the last run stopped, late arrivals included. It covers interactions recorded from the point the ordering became available, holds back the newest fifteen minutes, and does not accept date windows. An empty page means you are caught up; keep your saved cursor. Run the historical backfill once with the default ordering, then switch your schedule to order=received.Removed documents stay reportable
Removed documents stay reportable
Interactions with documents you have taken down stay in the feed, marked with
is_deleted as true. A trend line does not develop a hole when you retire a report.Limits to design around
Troubleshooting
403 Forbidden
403 Forbidden
The interactions endpoint requires the Admin role. If the key is bound to a service account, raise that service account to Admin or issue the key under an admin owner.
400 Invalid cursor
400 Invalid cursor
The cursor was altered, truncated, or built by hand. Pass back the
next_cursor value exactly as it was returned, and start a fresh walk with no cursor if you need to recover.429 Too Many Requests
429 Too Many Requests
You have exceeded the hourly or burst rate limit. Increase
REQUEST_PAUSE_SECONDS, and split a large backfill across several runs.The dataframe is empty
The dataframe is empty
Check the date window first.
start_date and end_date are microseconds, and passing seconds or milliseconds produces a window that matches nothing. Then confirm your trust center was live during the period you asked for.Many rows have an empty user_email
Many rows have an empty user_email
This is expected on a public trust center rather than a fault in the extract. A visitor can read a public document without identifying themselves, and a row gains an email only once someone verifies their identity, so re-pulling a trailing window fills in more of them over time.Group by
account_domain for account level reporting, or filter on has_known_person when you specifically need named individuals. The three levels of identification are explained in how much of the activity is identified.Rows show account_domain as unattributed
Rows show account_domain as unattributed
Same cause, one level up. The visit was not tied to any company, usually because the reader arrived on a public page and never requested access. The sample code labels these
unattributed so nothing is silently dropped from your totals. Filter on has_known_account to exclude them from account level views.Adapting this to other tools
Nothing here is specific to Hex beyond where the credential lives. The same requests work from any scheduler or notebook that can hold a secret and make HTTPS calls, including Databricks, Snowflake external functions, Airflow, and dbt Python models. Replace the bareWOLFIA_API_KEY reference with your platform’s own secret lookup, which on most of them is os.environ["WOLFIA_API_KEY"], and keep the rest of the code as it is.
Next steps
Trust analytics API
The full endpoint reference, including revenue attribution, active accounts, and access requests.
API overview
Authentication, rate limits, status codes, and every endpoint area.
Service accounts
Bind API keys to a non-human identity so scheduled jobs survive team changes.
Trust Center
Set up documents, branding, and access controls on the trust center itself.

