> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livingsecurity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Power BI

> Verbind Power BI met de Living Security Public API via Power Query, bouw een star-schema-model op /v1 en kies tussen operationele en rapportagedatasets voor phishing.

Deze handleiding is een end-to-end voorbeeld van het inlezen van `/v1` in Power BI. De handleiding behandelt
het connectiviteitspatroon van Power Query (M), een semantisch star-schema-model
dat is gebouwd op `/v1/activities`, incrementele vernieuwing en wanneer u de
**reporting**-previewdatasets gebruikt voor phishingresultaten per persoon.

De voorbeelden gebruiken de Amerikaanse productiehost
(`https://api.us-east-1.livingsecurity.ai/v1`). Gebruikt uw organisatie de EU-omgeving, gebruik dan
`https://api.eu-west-1.livingsecurity.ai/v1` in zowel `Fn_FetchAll` als
`Fn_GetJson` en koppel vervolgens de referentie van de gegevensbron aan die host. Zie
[Regio's](/nl/developers/v1/regions).

Deze handleiding gebruikt opgeslagen Basic-referenties met een API-sleutel. U hebt geen
OAuth-client nodig. Zie [Authenticatie](/nl/developers/v1/authentication) voor de
keuzes qua referenties en [Developer Hub](/nl/developers/developer-hub/overview)
voor toegang en configuratie.

## Waarvoor dit voorbeeld dient

Gebruik het wanneer u:

* Lijstendpoints van `/v1` wilt laden in een Power BI-dataset met correcte cursorpaginering en een nieuwe poging bij een rate limit.
* Een semantisch model wilt bouwen waarin metingen ("actieve entiteiten", "activiteiten per
  actieve entiteit") dimensionaal zijn in plaats van geteld vanuit een platte tabel.
* Een geplande Power BI-vernieuwing wilt terugbrengen van een volledige doorloop van de geschiedenis naar
  een klein dagelijks venster met behulp van incrementele vernieuwing.
* De juiste phishinggranulariteit wilt kiezen: campagnetotalen (operationeel) versus
  resultaten per persoon en getypeerde gebeurtenissen (reporting preview).

## Vereisten

* Een API-sleutel met de rechten die uw query's nodig hebben:
  * Operationeel voorbeeld: `activity:read`, `entity:read` en
    `simulation:read`.
  * Phishing per persoon via rapportagedatasets: `reporting:read`.
    Zie [API-sleutels](/nl/developers/developer-hub/api-keys).
* Power BI Desktop, of een Power BI-/Fabric-werkruimte waarin u een
  semantisch model kunt maken en vernieuwing kunt plannen.

## Kies de juiste granulariteit

`/v1` biedt twee verwante oppervlakken voor phishing. Ze zijn niet uitwisselbaar.

| Behoefte | Gebruik | Recht | Granulariteit |
| - | - | - | - |
| Totalen van de campagnetrechter | `GET /v1/simulations/{id}/results` | `simulation:read` | Eén rij per simulatie (afgeleverd, geopend, geklikt, …) |
| Resultaten per persoon (operationeel) | `GET /v1/simulation_results` | `simulation:read` | Eén rij per entiteit × simulatie |
| Resultaten per persoon (BI / rapportage) | `GET /v1/reporting/simulation_results` | `reporting:read` | Zelfde granulariteit, BI-veldnamen (`entity_id`, `simulation_id`), `updated_after`-watermark |
| Getypeerde interactiefeiten | `GET /v1/reporting/simulation_events` | `reporting:read` | Eén feit per interactie (`simulation.link_clicked`, …), `occurred_after` |
| Huidige Personeelsbestand-dimensie | `GET /v1/reporting/entities` | `reporting:read` | Eén huidige rij per entiteit. Koppel via `reporting.simulation_results.entity_id = reporting.entities.id` |
| Huidige entiteitsversies | `GET /v1/reporting/entity_versions` | `reporting:read` | Eén openeinde huidig interval per entiteit. Er bestaat geen tabel met versiegeschiedenis |
| Huidige Cohort-lidmaatschappen | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Eén huidig feit per `(cohort_id, entity_id)`. Er bestaat geen tabel met lidmaatschapsgeschiedenis |
| Koppelingsruggengraat voor alle activiteitstypen | `GET /v1/activities` | `activity:read` | Eén gebeurtenis per activiteit, genest `participants[]` |

De onderstaande Power BI-voorbeeldquery's roepen de **operationele** routes aan
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Geef voor rapportage van phishing per persoon de voorkeur aan de
onderstaande **reporting**-previewfamilie. Een sleutel die alleen `reporting:read` heeft gekregen, kan
operationele `/v1/simulation_results` niet aanroepen (de API retourneert `403`).

### Moeilijkheidsschaal

De moeilijkheidsgraad van een simulatie is een geheel getal van 1–5 op `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), eigenschappen van `/v1/activities`
(`difficulty_tier`) en `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` is het makkelijkst en `5` het moeilijkst. Adaptieve en progressieve simulaties hebben geen
enkele waarde op campagneniveau, dus `/v1/simulations.difficulty_level` is `null`,
terwijl `difficulty_level` per ontvanger in de resultaten nog wel kan zijn ingesteld. Om te mappen naar
oudere low/medium/high-exports behandelt u `1–2` als low, `3` als medium en `4–5` als
high.

<Warning>
  Bouw geen nieuwe BI-integraties op basis van ongedocumenteerde netwerkaanroepen van het Dashboard
  of het verouderde, samengestelde `/api/v1` Public API-overzicht. Authenticeer bij de
  regionale `/v1`-host van de organisatie met een API-sleutel uit de Developer Hub. Zie
  [Regio's](/nl/developers/v1/regions).
</Warning>

## Connectiviteitspatroon (Power Query)

Vier regels liggen ten grondslag aan elke query in het voorbeeld. Elke regel wordt afgedwongen door de
Power BI-service en is dus geen kwestie van stijlvoorkeur.

<AccordionGroup>
  <Accordion title="Houd de basis-URL van Web.Contents statisch en laat deze eindigen op /v1">
    Plaats alles wat dynamisch is in `RelativePath` en `Query`. Het eerste argument van
    `Web.Contents` moet een letterlijke tekenreeks zijn die eindigt bij de regionale
    `/v1`-root van de organisatie:

    * VS: `https://api.us-east-1.livingsecurity.ai/v1`
    * EU: `https://api.eu-west-1.livingsecurity.ai/v1`

    Aanroepers geven vervolgens alleen de resource door (`"simulations"`, niet
    `"v1/simulations"`). Plak geen Amerikaanse host voor een EU-organisatie.

    Als het eerste argument wordt opgebouwd door tekenreeksen samen te voegen, kan de Power BI-service
    de gegevensbron niet statisch identificeren en mislukt de geplande vernieuwing met
    een foutmelding over een "dynamic data source", ook al werkt de query in Power BI
    Desktop.
  </Accordion>

  <Accordion title="Lever de API-sleutel aan als opgeslagen Basic-referentie, niet in de querytekst">
    Wanneer Power BI om referenties vraagt voor de gegevensbron `/v1`, kies dan
    **Basic** en plak de API-sleutel als **gebruikersnaam**. Vul in het veld **Wachtwoord**
    een tijdelijke waarde in, zoals `x`. De Power BI-service weigert een leeg
    wachtwoord, ook al negeert de API dat veld. De sleutel als wachtwoord met een tijdelijke
    gebruikersnaam werkt ook.

    Voeg in M geen `Authorization`-header toe. Dat werkt op uw desktop, maar legt daarna de sleutel
    vast in het querybestand en werkt de geplande vernieuwing tegen.
  </Accordion>

  <Accordion title="Laat de verbindingstest van het referentiedialoogvenster uitvoeren">
    `GET /v1` is geauthenticeerd: het retourneert `200` met een geldige sleutel en `401`
    zonder sleutel. Richt de gegevensbron op `…/v1` en sla de test **niet** over —
    een goede sleutel wordt opgeslagen, een ingetrokken of onjuist opgemaakte sleutel mislukt direct.

    Oudere voorbeelden gebruikten de kale host als gegevensbron en adviseerden de test over te slaan, omdat `GET /` altijd `404` is. Richt die modellen opnieuw op `…/v1`.
  </Accordion>

  <Accordion title="Stel limit=500 expliciet in op elke lijstquery">
    De standaardpaginagrootte op elk lijstendpoint van `/v1` is 25. Als u de standaardwaarde laat staan,
    neemt het aantal verzoeken voor dezelfde gegevens sterk toe. Queryswaarden moeten tekst zijn —
    `limit = "500"` in M, niet `limit = 500`.
  </Accordion>
</AccordionGroup>

### Gedeelde pagineringsfunctie

Elke query in het voorbeeld roept één enkele `Fn_FetchAll` aan die
`next_page_token` doorloopt totdat deze `null` is (zie
[paginering](/nl/developers/v1/pagination)). De belangrijkste eigenschappen:

* **Statische basis-URL `/v1`, dynamische `RelativePath` en `Query`.** Vereist voor
  geplande vernieuwing en voor de referentietest.
* **`ManualStatusHandling = {429}` met een begrensd aantal nieuwe pogingen.** De functie leest
  `Retry-After` wanneer Power Query die beschikbaar stelt, wacht anders 30 seconden en
  geeft na 5 opeenvolgende `429`-antwoorden op met een expliciete fout, in plaats van
  te blijven herhalen totdat de vernieuwing een time-out krijgt. Op `5xx` wordt bewust geen nieuwe
  poging gedaan — een serverfout moet zichtbaar worden en mag niet ongemerkt een gedeeltelijke
  dataset opleveren.
* **`Timeout` van 60 seconden.** Pagina's antwoorden doorgaans ruim binnen een seconde, dus
  een minuut stilte betekent dat het verzoek is mislukt.
* **`Binary.Buffer` vóór `Json.Document`.** Zonder buffering kan de mashup-engine
  de responsestroom opnieuw opvragen als deze meer dan eens wordt gelezen.
* **`baseQuery` wordt bij elke iteratie ongewijzigd doorgegeven.** Paginatokens zijn
  ondoorzichtige, ondertekende cursors die zijn gebonden aan de organisatie, de referentie, de contractversie
  en de vingerafdruk van filters en sortering. Het wijzigen van een filter
  tijdens een doorloop wordt afgewezen als ongeldig token en niet opnieuw gestart vanaf pagina
  één.

```m Fn_FetchAll.pq — base and call shape theme={null}
let
    // Literal /v1 root. Callers pass the resource alone ("simulations").
    // US: "https://api.us-east-1.livingsecurity.ai/v1"
    BaseUrl = "https://api.eu-west-1.livingsecurity.ai/v1",

    Fn_FetchAll = (relativePath as text, baseQuery as record) as list =>
        // Walk next_page_token until null. Full pagination + 429 retry lives
        // in examples/powerbi/queries/Fn_FetchAll.pq.
        ...
in
    Fn_FetchAll
```

### Getypeerde query's per endpoint

Elke afnemer van `Fn_FetchAll` volgt dezelfde opzet: roep het endpoint aan,
projecteer een vaste kolomlijst, verwijder de discriminator `object` en type
tijdstempels als `datetimezone`, zodat datummetingen niet ongemerkt verschuiven door de tijdzone van de
vernieuwingscapaciteit.

<CodeGroup>
  ```m Simulations.pq (GET /v1/simulations) theme={null}
  let
      Source = Fn_FetchAll("simulations", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          {
              "id", "object", "name", "description", "status", "vectors",
              "difficulty_level", "difficulty_mode",
              "started_at", "completed_at", "created_at", "updated_at"
          },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Flattened = Table.TransformColumns(
          Dropped,
          {
              {
                  "vectors",
                  each if _ = null then null
                       else Text.Combine(List.Transform(_, Text.From), ", "),
                  type text
              }
          }),

      Typed = Table.TransformColumnTypes(
          Flattened,
          {
              { "id",           type text },
              { "name",         type text },
              { "description",  type text },
              { "status",       type text },
              { "started_at",   type datetimezone },
              { "completed_at", type datetimezone },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```

  ```m Entities.pq (GET /v1/entities) theme={null}
  let
      Source = Fn_FetchAll("entities", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          { "id", "object", "type", "status", "display_name", "email",
            "created_at", "updated_at" },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Typed = Table.TransformColumnTypes(
          Dropped,
          {
              { "id",           type text },
              { "type",         type text },
              { "status",       type text },
              { "display_name", type text },
              { "email",        type text },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```
</CodeGroup>

<Tip>
  Een expliciete kolomlijst op `Table.FromRecords` houdt het schema stabiel wanneer een
  endpoint nul rijen retourneert. Een kale `Table.FromRecords({})` levert een
  tabel zonder kolommen op en alle latere stappen mislukken door de ontbrekende velden.
</Tip>

## Excel

Dezelfde Power Query (M) werkt in Get & Transform van Excel — het is dezelfde
engine als in Power BI Desktop. Gebruik dit wanneer u een werkbladtabel wilt
in plaats van een semantisch model.

In Excel voor Windows (Microsoft 365, Excel 2024, 2021, 2019 of 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. Kies in de Power Query Editor **Home → Advanced Editor**. Plak
   eerst `Fn_FetchAll` en hernoem die query vervolgens tot `Fn_FetchAll` (Query
   Settings → Name, of klik met de rechtermuisknop in het deelvenster Queries op **Rename**).
3. Herhaal dit voor `Fn_GetJson` en daarna voor elke rapportquery.
4. **Home → Close & Load → Close & Load To…** opent het dialoogvenster **Import Data**.
   Kies **Only Create Connection** voor `Fn_FetchAll` en
   `Fn_GetJson` (het zijn functies, geen tabellen). Kies **Table** voor
   elke rapportquery die u op een werkblad wilt hebben.
5. Kies bij de eerste vernieuwing **Basic**: de API-sleutel als **User name** en een
   tijdelijke waarde zoals `x` als **Password**. Excel slaat één referentie op
   per URL van de gegevensbron; latere query's die dezelfde `/v1`-host aanroepen, hergebruiken
   die.

<Note>
  **Close & Load To…** kan gedimd zijn wanneer u voor het eerst een query in
  een werkmap maakt. Gebruik **Close & Load**, selecteer vervolgens in Excel **Data →
  Queries & Connections**, klik met de rechtermuisknop op de query en kies **Load To**.
  Dat pad wordt door Microsoft voor dit geval gedocumenteerd.
</Note>

U kunt `Living-Security-Version` niet vanuit Power Query verzenden op een
geauthenticeerde bron — dezelfde beperking als in Power BI. Zet het serviceaccount vast of geef
`api_version` mee in het queryrecord. Zie
[Contractversies en opgeslagen referenties](#contractversies-en-opgeslagen-referenties).

Kant-en-klare rapportquery's (kolomtoewijzingen en afwijkingen ten opzichte van de export van een
vorige aanbieder):

<CardGroup cols={3}>
  <Card title="Compliance per persoon" href="/nl/developers/v1/reports/per-person-compliance">
    Training-inschrijvingen op granulariteit persoon × toewijzing.
  </Card>

  <Card title="Logboek voor e-mailphishing" href="/nl/developers/v1/reports/email-phishing-log">
    Phishinginteracties op gebeurtenisniveau, afgelopen 12 maanden.
  </Card>

  <Card title="Meest vatbare gebruikers" href="/nl/developers/v1/reports/most-susceptible-users">
    Overzicht per persoon van klikken, referenties en meldingen over 12 maanden.
  </Card>
</CardGroup>

## Reporting-previewdatasets (phishing per persoon)

De reporting-previewfamilie biedt datasets voor phishing per persoon.
Hiervoor zijn `reporting:read` en rapportagetoegang voor uw organisatie vereist.
Gebruik de onderstaande datasetcatalogus om de schema's en databeleidsregels te bekijken.

Ontdek de live catalogus:

```text theme={null}
GET /v1/reporting/datasets
GET /v1/reporting/datasets/simulation_results
GET /v1/reporting/datasets/simulation_events
GET /v1/reporting/datasets/entities
GET /v1/reporting/datasets/entity_versions
GET /v1/reporting/datasets/cohort_memberships
```

Elke dataset declareert granulariteit, primaire sleutel, koppelingen, watermark, correctie- en
verwijderingsbeleid.

### simulation\_results

* **Pad:** `GET /v1/reporting/simulation_results`
* **Granulariteit:** entiteit × simulatie
* **Filters:** `simulation`, `entity`, `updated_after`, plus `limit` /
  `page_token`
* **Wire-object:** `reporting.simulation_result`
* **Koppelsleutels:** `entity_id`, `simulation_id`

```m ReportingSimulationResults.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_results",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id", "status",
            "targeted_at", "delivered_at", "opened_at", "clicked_at",
            "credential_submitted_at", "reported_at",
            "email_subject", "sender_email", "difficulty_level",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "status",        type text },
            { "targeted_at",   type datetimezone },
            { "updated_at",    type datetimezone }
        })
in
    Typed
```

Gebruik `updated_after` als inclusief ondergrenswatermark wanneer u recente
partities vernieuwt. Veranderlijke resultaatdimensies worden ter plekke opnieuw weergegeven onder dezelfde `id`.

### simulation\_events

* **Pad:** `GET /v1/reporting/simulation_events`
* **Granulariteit:** één getypeerd feit per waargenomen interactie
* **Filters:** `simulation`, `entity`, `event_type`, `occurred_after`, plus
  `limit` / `page_token`
* **Wire-object:** `reporting.simulation_event`
* **Gebeurtenistypen:** gesloten publieke tokens zoals `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — niet de ruwe
  ClickHouse-activiteitstypen van `phishing.simulation.*`
* **Apparaat:** `device_type` is de afgeleide klasse (waargenomen tokens zijn onder meer
  `desktop`, `mobile`, `tablet`, `unknown`). Ruwe `user_agent` staat niet op
  `/v1`. Het ruwe IP-adres staat nooit op `/v1` (nooit opgeslagen; alleen gezouten `ip_hash`).

```m ReportingSimulationEvents.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_events",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id",
            "event_type", "occurred_at",
            "email_subject", "sender_email", "sender_domain", "difficulty_tier",
            "device_type",
            "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "event_type",    type text },
            { "occurred_at",   type datetimezone }
        })
in
    Typed
```

Gebruik `occurred_after` voor incrementele gebeurtenisladingen. Feiten zijn alleen-toevoegen;
laat binnenkomende gegevens zijn nieuwe rijen.

### entities

* **Pad:** `GET /v1/reporting/entities`
* **Granulariteit:** één huidige rij per entiteit
* **Filters:** `type`, `updated_after`, plus `limit` / `page_token`
* **Wire-object:** `reporting.entity`
* **Koppelsleutel:** `id` / `entity_id` (dezelfde waarde)

Koppel phishingfeiten als volgt aan personen:
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operationele `GET /v1/entities` blijft op `entity:read` en is een ander
recht.

```m ReportingEntities.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entities", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status",
            "display_name", "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",          type text },
            { "entity_id",   type text },
            { "type",        type text },
            { "status",      type text },
            { "display_name", type text },
            { "created_at",  type datetimezone },
            { "updated_at",  type datetimezone }
        })
in
    Typed
```

### entity\_versions en cohort\_memberships

Deze twee datasets kennen een benoemde uitzondering met alleen de huidige stand:

> Er bestaat geen gezaghebbende tabel met versie- of lidmaatschapsgeschiedenis. Deze dataset projecteert de huidige rij als één openeinde interval (`valid_to` is null, `is_current` is true). `valid_from` is `created_at` of anders `updated_at` voor entiteitsversies, en het waarnemingstijdstip van het verzoek voor Cohort-lidmaatschappen. Gesloten historische intervallen worden niet verzonnen.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Dezelfde filters als
  entities. `valid_from` is `created_at` of anders `updated_at`. Een entiteit
  waarbij beide tijdstempels null zijn, wordt weggelaten.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filters:
  `cohort`, `entity`, `updated_after`. `updated_at` is de laatste update van de Cohort,
  zodat incrementele vernieuwing betrouwbaar blijft. `valid_from` is het waarnemingstijdstip
  van het verzoek. Koppel `entity_id` aan `reporting.entities.id`.

```m ReportingEntityVersions.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entity_versions", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status", "display_name",
            "valid_from", "valid_to", "is_current",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical }
        })
in
    Typed
```

```m ReportingCohortMemberships.pq theme={null}
let
    Source = Fn_FetchAll("reporting/cohort_memberships", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "cohort_id", "entity_id",
            "valid_from", "valid_to", "is_current",
            "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "cohort_id",  type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical },
            { "updated_at", type datetimezone }
        })
in
    Typed
```

## Semantisch star-schema-model op /v1/activities

`/v1/activities` is de gebeurtenisstroom met tijdstempels van de organisatie. Er zijn geen
numerieke metingen — elke hoeveelheid in een rapport is een aantal rijen — waardoor een
star schema een natuurlijke keuze is.

```text theme={null}
Dates (dimension)
      │  1
      ▼  ∗
  Activities (fact)   1  ∗  ActivityParticipants (bridge)   ∗  1   Entities (dimension)
```

* **`Activities`** is het gebeurtenisfeit op de werkelijke granulariteit. Eén rij per activiteit.
  Deelnemers worden hier weggelaten en opnieuw opgebouwd als een eigen tabel.
* **`ActivityParticipants`** bestaat omdat deelnemers daadwerkelijk
  een-op-veel zijn, met rollen zoals `actor` en `target`. Afvlakken op
  `Activities` zou ofwel activiteitsrijen dupliceren (waardoor elk aantal wordt opgeblazen)
  ofwel extra deelnemers verliezen.
  De relatie met `Activities` is de enige bidirectionele
  relatie van het model, zodat een entiteitsslicer de feitentabel bereikt.
* **`Entities`** is de dimensie die deelnemers benoemt. Deelnemersrijen
  bij een activiteit bevatten `entity_id` en een rol, maar geen weergavenaam.
* **`Dates`** wordt gegenereerd in M (nul API-aanroepen) en gemarkeerd als de datumtabel
  van het model. Daardoor is DAX-tijdintelligentie
  (`DATESINPERIOD`, `DATEADD`) toegestaan. Kolommen met maand- en weekdagnamen declareren
  `sortByColumn` op hun numerieke tegenhangers, zodat visuals maanden niet
  alfabetisch sorteren.

Alle metingen zijn expliciet (het model stelt `discourageImplicitMeasures` in): totaal aantal
activiteiten, actieve entiteiten, activiteiten per actieve entiteit, dagen met activiteit
en een daggemiddelde over de afgelopen 7 dagen.

### Opbouw van de brugtabel

`ActivityParticipants` doorloopt `/v1/activities` en splitst de array `participants`
uit naar één rij per deelnemer:

```m ActivityParticipants.pq theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Selected = Table.SelectColumns(AsTable, { "id", "event_ts", "participants" }),
    Renamed  = Table.RenameColumns(Selected, { { "id", "activity_id" } }),
    Expanded = Table.ExpandListColumn(Renamed, "participants"),
    NonNull  = Table.SelectRows(Expanded, each [participants] <> null),
    Fields   = Table.ExpandRecordColumn(
        NonNull, "participants",
        { "role", "entity_id", "display_name" }),

    Typed = Table.TransformColumnTypes(
        Fields,
        {
            { "activity_id",  type text },
            { "event_ts",     type datetimezone },
            { "role",         type text },
            { "entity_id",    type text },
            { "display_name", type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd)
in
    Filtered
```

`ActivityParticipants` doorloopt `/v1/activities` onafhankelijk, in plaats van
te verwijzen naar de query `Activities`. In Power BI haalt elke van twee query's die als twee
tabellen zijn geladen tijdens de vernieuwing zelfstandig gegevens op. Door ze onafhankelijk te houden
kan de service de partities van beide tabellen parallel vernieuwen.

## Incrementele vernieuwing

Beide tabellen op feitniveau (`Activities` en `ActivityParticipants`) declareren een
beleid voor incrementele vernieuwing van Power BI: bewaar 5 jaar aan jaarlijkse partities
en importeer bij elke vernieuwing de laatste 14 dagen opnieuw.

Het mechanisme is een paar DateTime-parameters, `RangeStart` en `RangeEnd`,
die de Power BI-service tijdens de vernieuwing per partitie opnieuw bindt. Elke feitenquery
verwerkt die parameters in de API-filters `start_date` en `end_date`,
zodat het aantal verzoeken per partitie evenredig is met de partitie
en niet met de totale geschiedenis. De query verbreedt het API-venster aan elke kant met een minuut
en past de exacte halfopen `[RangeStart, RangeEnd)`-grens lokaal opnieuw toe,
zodat de juistheid van de partitie niet afhangt van de vraag of de server
zijn grenzen inclusief of exclusief behandelt.

```m Activities.pq — the fold that makes incremental refresh work theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object", "participants" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "activity_type", type text },
            { "event_ts",      type datetimezone },
            { "service_id",    type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd),

    WithDate = Table.AddColumn(
        Filtered, "event_date", each Date.From([event_ts]), type date)
in
    WithDate
```

De eerste vernieuwing na een implementatie past het beleid toe: een volledige historische lading,
traag. Elke volgende vernieuwing raakt alleen het incrementele venster.

Gemeten op een dataset van \~158.000 activiteiten en \~2.300 entiteiten:

| Vernieuwing | Doorlooptijd |
| - | - |
| Eerste (toepassing van beleid, volledige historische lading) | \~31 min |
| Elke volgende vernieuwing | **\~15 s** |

<Warning>
  Het klassieke REST-endpoint voor vernieuwing van Power BI (`POST .../refreshes` met alleen
  `notifyOption`) negeert vernieuwingsbeleid stilzwijgend en doorloopt elke keer alles. Om het beleid via de REST API toe te passen, gebruikt u
  enhanced refresh: verzend `type`, `commitMode` en `applyRefreshPolicy` in de body. Vernieuwingen
  die door de scheduler van de Power BI-service worden gestart, passen beleid altijd toe — deze
  eigenaardigheid geldt alleen voor de REST-aanroep.
</Warning>

<Note>
  Het venster van 14 dagen is bewust ruim. Activiteitsinname is niet alleen-toevoegen-op-het-moment:
  backfills van connectoren leveren gebeurtenissen met tijdstempels uit het verleden.
  Met een venster van 1-2 dagen zou u die blijvend missen.
</Note>

## Het vernieuwingsvenster doorgeven aan de API

`GET /v1/activities` accepteert de filters `start_date` en `end_date` waarin de
Power BI-parameters worden verwerkt en filtert `event_ts` tussen de grenzen.
`Entities` is een dimensie, dus in dit voorbeeld wordt deze telkens volledig vernieuwd
in plaats van incrementeel.

Rapportagedatasets gebruiken hun eigen watermarks (`updated_after`,
`occurred_after`) in plaats van het datumvenster van activiteiten.

## Rate limits tijdens vernieuwing

`/v1` hanteert afzonderlijke budgetten per sleutel en per IP (zie
[rate limits](/nl/developers/v1/rate-limits)). Houd rekening met `429` + `Retry-After`; codeer
geen verzoekbudget hard in uw model. Twee eigenschappen van Power BI-vernieuwingen
zijn van belang:

* **Het doorlopen van één tabel is sequentieel.** Elk paginaverzoek heeft het
  token van de vorige pagina nodig, dus één tabel is latentiegebonden, ongeacht de paginagrootte.
* **Gelijktijdige tabelvernieuwingen vermenigvuldigen zich.** Meerdere grote tabellen die
  parallel worden vernieuwd, vormen het geval dat het plafond raakt, en daarom is het 429-
  herpoging in `Fn_FetchAll` van belang. Geef de voorkeur aan minder parallelle tabellen boven
  de aanname van onbeperkte gelijktijdigheid.

De herpoging is getest tegen een echt verzadigde limiter. Een vernieuwing
die liep terwijl een parallelle workload de sleutel bewust op zijn plafond hield,
werd toch voltooid en leverde dezelfde aantallen rijen op als een ongestoorde basislijn —
herpogingen leverden volledige pagina's op, geen hiaten.

## Contractversies en opgeslagen referenties

`/v1` accepteert een requestheader `Living-Security-Version` om een gepubliceerde
contractversie vast te zetten (zie [API-quickstart](/nl/developers/v1/quickstart)). Power
Query kan die header niet verzenden wanneer de gegevensbron opgeslagen referenties gebruikt —
alleen anonieme gegevensbronnen staan aangepaste headers toe. Als u de header toevoegt, mislukt een vernieuwing
met:

```text theme={null}
Expression.Error: The 'Living-Security-Version' header is only supported when
connecting anonymously.
Microsoft.Data.Mashup.ErrorCode = 10348.
```

Bewaar de sleutel in de opgeslagen referenties van Power BI en laat de aangepaste header weg.
Zonder expliciete versie in het verzoek gebruikt de API de vastgezette `contract_version`
van het serviceaccount, of de nieuwste gepubliceerde versie als er geen vastzetting is ingesteld.
Controleer de contractversie van het account bij uw API-beheerder voordat u
een vernieuwing publiceert. Zie het [API-wijzigingslogboek](/nl/developers/v1/changelog) voor
ondersteunde versies.

Als een vernieuwing zich onverwacht gaat gedragen, controleer dan de
responseheader `Living-Security-Version` van de API om te zien welk contract het verzoek
daadwerkelijk heeft afgehandeld.

## Uw model vernieuwen en verifiëren

Publiceer het semantische model in uw Power BI-werkruimte, koppel de opgeslagen
referentie aan de regionale bron `/v1` en voer een vernieuwing uit voordat u een
schema inschakelt. Controleer de vernieuwingsgeschiedenis op fouten of onvolledige leesacties.

Controleer het aantal rijen aan de hand van een onafhankelijke API-leesactie met dezelfde filters.
De [pagineringshandleiding](/nl/developers/v1/pagination) bevat Python- en cURL-voorbeelden
voor het lezen van een volledige resultatenset. Een vernieuwing die alleen de
eerste pagina leest, kan bij een kleine dataset geslaagd lijken; vergelijk de aantallen voordat u
op het rapport vertrouwt.

## Modelkeuzes

* **Campagnetotalen en resultaten per persoon hebben een verschillende granulariteit.**
  `SimulationResults` roept `GET /v1/simulations/{id}/results` aan
  (één GET per uitvoering). Voeg voor phishing per persoon query's toe voor
  `GET /v1/reporting/simulation_results` en
  `GET /v1/reporting/simulation_events` (of operationele
  `GET /v1/simulation_results` als u al over `simulation:read` beschikt).
* **`vectors` op `/v1/simulations` wordt afgevlakt tot een kommagescheiden tekenreeks.**
  Een kolom van het type lijst kan niet als echte dimensie worden gesliced. Als een rapportpagina
  op vector moet filteren, voegt u een brugtabel van simulatie naar vector toe.

## Volgende stappen

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/nl/developers/developer-hub/overview">
    Maak een serviceaccount en API-sleutel voor Power BI.
  </Card>

  <Card title="API-quickstart" href="/nl/developers/v1/quickstart">
    Authenticeer `/v1`-verzoeken met Basic-authenticatie.
  </Card>

  <Card title="Paginering" href="/nl/developers/v1/pagination">
    De lijstenvelop, cursortokens en filterstabiliteit tijdens een doorloop.
  </Card>

  <Card title="Rate limits" href="/nl/developers/v1/rate-limits">
    Budgetten per sleutel en per IP, het 429-contract en richtlijnen voor herpogingen.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.