/v1 into Power BI. It covers
the Power Query (M) connectivity pattern, a star-schema semantic model built
on /v1/activities, incremental refresh, and when to use the reporting
preview datasets for per-person phishing outcomes.
The examples use the US production host
(https://api.us-east-1.livingsecurity.ai/v1). For an EU organization, use
https://api.eu-west-1.livingsecurity.ai/v1 in both Fn_FetchAll and
Fn_GetJson, then bind the data-source credential to that host. See
Regions.
This guide uses stored Basic credentials with an API key. You do not need an
OAuth client. See Authentication for the
credential choices, and Developer Hub
for access and setup.
What this example is for
Use it when you need to:- Load
/v1list endpoints into a Power BI dataset with correct cursor pagination and a rate-limit retry. - Build a semantic model where measures (“active entities”, “activities per active entity”) are dimensional rather than counted off a flat table.
- Take a scheduled Power BI refresh from a full walk of history down to a small daily window using incremental refresh.
- Choose the right phishing grain: campaign totals (operational) versus per-person results and typed events (reporting preview).
Prerequisites
- An API key with the permissions your queries need:
- Operational example:
activity:read,entity:read, andsimulation:read. - Per-person phishing via reporting datasets:
reporting:read. See API keys.
- Operational example:
- Power BI Desktop, or a Power BI / Fabric workspace where you can create a semantic model and schedule refresh.
Choose the right grain
/v1 exposes two related surfaces for phishing. They are not interchangeable.
The Power BI example queries below call the operational routes
(
/v1/simulations, /v1/simulations/{id}/results, /v1/activities,
/v1/entities). For per-person phishing reporting, prefer the
reporting preview family below. A key granted only reporting:read cannot
call operational /v1/simulation_results (the API returns 403).
Difficulty scale
Simulation difficulty is a 1–5 integer on/v1/simulations (difficulty_level),
/v1/simulation_results (difficulty_level), /v1/activities properties
(difficulty_tier), and /v1/reporting/simulation_events (difficulty_tier).
1 is easiest and 5 is hardest. Adaptive and progressive simulations have no
single campaign-level value, so /v1/simulations.difficulty_level is null
while per-recipient difficulty_level on results can still be set. To map onto
legacy low/medium/high exports, treat 1–2 as low, 3 as medium, and 4–5 as
high.
Connectivity pattern (Power Query)
Four rules underpin every query in the example. Each one is enforced by the Power BI Service rather than by style preferences.Keep the Web.Contents base URL static and end it at /v1
Keep the Web.Contents base URL static and end it at /v1
Put everything dynamic in
RelativePath and Query. The first argument to
Web.Contents must be a literal string that ends at the organization’s
regional /v1 root:- US:
https://api.us-east-1.livingsecurity.ai/v1 - EU:
https://api.eu-west-1.livingsecurity.ai/v1
"simulations", not
"v1/simulations"). Do not paste a US host for an EU organization.If the first argument is built by string concatenation, the Power BI Service
cannot statically identify the data source and scheduled refresh fails with
a “dynamic data source” error, even though the query works in Power BI
Desktop.Supply the API key as a stored Basic credential, not in query text
Supply the API key as a stored Basic credential, not in query text
When Power BI prompts for credentials on the
/v1 data source, choose
Basic and paste the API key as the username. Put a placeholder such
as x in the password field. The Power BI Service rejects a blank
password even though the API ignores that field. Key as password with a
placeholder username also works.Do not add an Authorization header in M. It works on your desktop, then
bakes the key into the query file and fights scheduled refresh.Let the credential dialog's test connection run
Let the credential dialog's test connection run
GET /v1 is authenticated: it returns 200 with a valid key and 401
without one. Point the data source at …/v1 and do not skip the test —
a good key saves, a revoked or malformed one fails immediately.Older examples used the bare host as the data source and told you to skip
the test because GET / is always 404. Re-point those models at …/v1.Set limit=500 explicitly on every list query
Set limit=500 explicitly on every list query
The default page size on every
/v1 list endpoint is 25. Leaving it default
multiplies request count for the same data. Query values must be text —
limit = "500" in M, not limit = 500.Shared pagination function
Every query in the example calls a singleFn_FetchAll that walks
next_page_token until it is null (see
pagination). Its notable properties:
- Static
/v1base URL, dynamicRelativePathandQuery. Required for scheduled refresh and for the credential test. ManualStatusHandling = {429}with a bounded retry. The function readsRetry-Afterwhen Power Query exposes it, otherwise waits 30 seconds, and gives up after 5 consecutive429responses with an explicit error rather than looping until the refresh times out.5xxis deliberately not retried — a server error should surface, not silently produce a partial dataset.- 60-second
Timeout. Pages typically answer in well under a second, so a minute of silence means the request is dead. Binary.BufferbeforeJson.Document. Without buffering, the mashup engine may re-request the response stream if it reads it more than once.baseQueryis passed unchanged every iteration. Page tokens are opaque, signed cursors bound to the organization, credential, contract version, and the fingerprint of filters and sort. Changing a filter mid-walk is rejected as an invalid token rather than restarted from page one.
Fn_FetchAll.pq — base and call shape
Typed queries per endpoint
Every consumer ofFn_FetchAll follows the same shape: call the endpoint,
project a fixed column list, drop the object discriminator, and type
timestamps as datetimezone so date measures do not silently shift by the
refresh capacity’s timezone.
Excel
The same Power Query (M) runs in Excel’s Get & Transform — it is the same engine as Power BI Desktop. Use this when you want a worksheet table instead of a semantic model. On Excel for Windows (Microsoft 365, Excel 2024, 2021, 2019, or 2016):- Data → Get Data → From Other Sources → Blank Query.
- In the Power Query Editor, Home → Advanced Editor. Paste
Fn_FetchAllfirst, then rename that queryFn_FetchAll(Query Settings → Name, or right-click in the Queries pane → Rename). - Repeat for
Fn_GetJson, then for each report query. - Home → Close & Load → Close & Load To… opens the Import Data
dialog. Choose Only Create Connection for
Fn_FetchAllandFn_GetJson(they are functions, not tables). Choose Table for each report query you want on a worksheet. - On first refresh, choose Basic: API key as User name, a
placeholder such as
xas Password. Excel stores one credential per data source URL; later queries that hit the same/v1host reuse it.
Close & Load To… can be dimmed the first time you create a query in
a workbook. Use Close & Load, then in Excel select Data →
Queries & Connections, right-click the query, and choose Load To.
That path is documented by Microsoft for this case.
Living-Security-Version from Power Query on an
authenticated source — the same restriction as Power BI. Pin the service
account or pass api_version in the query record. See
Contract versions and stored credentials.
Ready-made report queries (column mappings and divergences from a
previous provider’s export):
Per-person compliance
Training enrollments at person × assignment grain.
Email phishing log
Event-level phishing interactions, trailing 12 months.
Most susceptible users
Per-person 12-month click, credential, and report rollup.
Reporting preview datasets (per-person phishing)
The reporting preview family provides datasets for per-person phishing. It requiresreporting:read and reporting access for your organization.
Use the dataset catalog below to inspect its schemas and data policies.
Discover the live catalog:
simulation_results
- Path:
GET /v1/reporting/simulation_results - Grain: entity × simulation
- Filters:
simulation,entity,updated_after, pluslimit/page_token - Wire object:
reporting.simulation_result - Join keys:
entity_id,simulation_id
ReportingSimulationResults.pq (sketch)
updated_after as the inclusive lower watermark when you refresh recent
partitions. Mutable result dimensions restate in place under the same id.
simulation_events
- Path:
GET /v1/reporting/simulation_events - Grain: one typed fact per observed interaction
- Filters:
simulation,entity,event_type,occurred_after, pluslimit/page_token - Wire object:
reporting.simulation_event - Event types: closed public tokens such as
simulation.delivered,simulation.opened,simulation.link_clicked,simulation.credentials_submitted,simulation.reported— not raw ClickHousephishing.simulation.*activity types - Device:
device_typeis the derived class (observed tokens includedesktop,mobile,tablet,unknown). Rawuser_agentis not on/v1. Raw IP is never on/v1(never persisted; saltedip_hashonly).
ReportingSimulationEvents.pq (sketch)
occurred_after for incremental event loads. Facts are append-only;
late arrivals are new rows.
entities
- Path:
GET /v1/reporting/entities - Grain: one current row per entity
- Filters:
type,updated_after, pluslimit/page_token - Wire object:
reporting.entity - Join key:
id/entity_id(same value)
reporting.simulation_results.entity_id = reporting.entities.id.
Operational GET /v1/entities stays on entity:read and is a different
permission.
ReportingEntities.pq
entity_versions and cohort_memberships
These two datasets ship a named current-only exception:No authoritative version or membership-history table exists. This dataset projects the current row as one open-ended interval (valid_tois null,is_currentis true).valid_fromiscreated_ator elseupdated_atfor entity versions, and the request observation time for cohort memberships. Closed historical intervals are not fabricated.
- entity_versions:
GET /v1/reporting/entity_versions. Same filters as entities.valid_fromiscreated_ator elseupdated_at. An entity with both timestamps null is omitted. - cohort_memberships:
GET /v1/reporting/cohort_memberships. Filters:cohort,entity,updated_after.updated_atis the cohort last-update so incremental refresh stays honest.valid_fromis the request observation time. Joinentity_idtoreporting.entities.id.
ReportingEntityVersions.pq
ReportingCohortMemberships.pq
Star-schema semantic model on /v1/activities
/v1/activities is the organization’s timestamped event stream. It has no
numeric measures — every quantity in a report is a row count — which makes a
star schema a natural fit.
Activitiesis the event fact at true grain. One row per activity. Participants are dropped here and rebuilt as their own table.ActivityParticipantsexists because participants are genuinely one-to-many, with roles such asactorandtarget. Flattening ontoActivitieswould either duplicate activity rows (inflating every count) or discard additional participants. The relationship toActivitiesis the model’s only bidirectional relationship, so an entity slicer reaches the fact table.Entitiesis the dimension that names participants. Participant rows on an activity carryentity_idand a role but not a display name.Datesis generated in M (zero API calls) and marked as the model’s date table. That is what makes DAX time intelligence (DATESINPERIOD,DATEADD) legal. Month and weekday name columns declaresortByColumnon their number twins so visuals do not sort months alphabetically.
discourageImplicitMeasures): total
activities, active entities, activities per active entity, days with activity,
and a 7-day trailing daily average.
Bridge table shape
ActivityParticipants walks /v1/activities and explodes the participants
array to one row per participant:
ActivityParticipants.pq
ActivityParticipants walks /v1/activities independently rather than
referencing the Activities query. In Power BI, two queries loaded as two
tables each fetch independently at refresh time. Keeping them independent
lets the service refresh both tables’ partitions in parallel.
Incremental refresh
Both fact-grain tables (Activities and ActivityParticipants) declare a
Power BI incremental refresh policy: keep 5 years of yearly partitions,
re-import the last 14 days on every refresh.
The mechanism is a pair of DateTime parameters, RangeStart and RangeEnd,
that the Power BI service rebinds per partition at refresh time. Each fact
query folds those parameters into the API’s start_date and end_date
filters, so partition-level request volume is proportional to the partition,
not to total history. The query widens the API window by a minute on each
side and re-applies the exact half-open [RangeStart, RangeEnd) boundary
locally, so partition correctness does not depend on whether the server
treats its bounds as inclusive or exclusive.
Activities.pq — the fold that makes incremental refresh work
The 14-day window is deliberately generous. Activity ingestion is not
append-only-at-now: connector backfills land events with past timestamps.
A 1-2 day window would permanently miss them.
Widening the refresh window into the API
GET /v1/activities accepts the start_date and end_date filters that the
Power BI parameters fold into, filtering event_ts between the bounds.
Entities is a dimension, so this example refreshes it in full each time
rather than incrementally.
Reporting datasets use their own watermarks (updated_after,
occurred_after) instead of the activities date window.
Rate limits during refresh
/v1 enforces separate per-key and per-IP budgets (see
rate limits). Honor 429 + Retry-After; do
not hard-code a request budget into your model. Two properties of Power BI
refreshes matter:
- A single table walk is sequential. Each page request needs the previous page’s token, so one table is latency-bound regardless of page size.
- Concurrent table refreshes multiply. Several large tables refreshing
in parallel is the case that hits the ceiling, which is why the 429
retry path in
Fn_FetchAllmatters. Prefer fewer parallel tables over assuming unlimited concurrency.
Contract versions and stored credentials
/v1 accepts a Living-Security-Version request header to pin a published
contract version (see API quickstart). Power
Query cannot send that header when the data source uses stored credentials —
only anonymous data sources allow custom headers. Adding it fails a refresh
with:
contract_version, or the latest published version if no pin is set.
Confirm the account’s contract version with your API administrator before
publishing a refresh. See the API changelog for
supported versions.
If a refresh starts behaving unexpectedly, inspect the API’s
Living-Security-Version response header to see which contract actually
served the request.
Refresh and verify your model
Publish the semantic model to your Power BI workspace, bind its stored credential to the regional/v1 source, and run a refresh before enabling a
schedule. Review the refresh history for failures or incomplete reads.
Check row counts against an independent API read using the same filters.
The pagination guide includes Python and cURL
examples for reading a complete result set. A refresh that reads only the
first page can look successful on a small dataset; compare counts before
relying on the report.
Model choices
- Campaign totals and per-person results have different grains.
SimulationResultscallsGET /v1/simulations/{id}/results(one GET per run). For per-person phishing, add queries forGET /v1/reporting/simulation_resultsandGET /v1/reporting/simulation_events(or operationalGET /v1/simulation_resultsif you already holdsimulation:read). vectorson/v1/simulationsis flattened to a comma-joined string. A list-typed column cannot be sliced as a real dimension. If a report page needs to filter by vector, add a simulation-to-vector bridge table.
Next steps
Developer Hub
Create a service account and API key for Power BI.
API quickstart
Authenticate
/v1 requests with Basic auth.Pagination
The list envelope, cursor tokens, and filter stability across a walk.
Rate limits
Per-key and per-IP budgets, the 429 contract, and retry guidance.

