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

# List event properties

> The catalog of custom `event_data` property keys this workspace sends, grouped by event name, each with how often it appeared, how many distinct values it took, one sample value, and its JSON type. Use it before `property_filters` on `/events`.

The counts come from a bounded SAMPLE of the most recent events in the window, not from the whole window: they rank keys, they do not measure them, and a low-volume event's keys can be missed entirely. Pass `event_name` to spend the whole sample on one event, which is the reliable way to enumerate a rare event's properties.

Top-level keys only. A nested object appears once with `value_type` `object`; its inner keys are not listed, and `/events` cannot filter on them either.



## OpenAPI

````yaml /api-reference/read-api.yaml get /events/properties
openapi: 3.1.0
info:
  title: Datalyr API
  version: 1.0.0
  description: >-
    Read analytics, attribution, revenue and workspace data from Datalyr, and
    change conversion rules, trackable links and container-script drafts under
    an explicit write scope.
servers:
  - url: https://api.datalyr.com/v1
security:
  - bearerAuth: []
tags:
  - name: Analytics
  - name: Events and users
  - name: Attribution and revenue
  - name: Workspace
  - name: Signup and onboarding
    description: >-
      The two routes an agent needs to take a prospect from nothing to a working
      install: one to start a signup, one to check what is still missing.


      **Agents sign up. Humans pay.** `POST /signup` is the only unauthenticated
      route in this API, and the only thing it does is send a verification
      email. It does not create an account, a workspace, a plan or an API key,
      and no parameter makes it. The person who reads that email verifies it,
      chooses a plan, and completes an Autumn-hosted Stripe checkout in their
      own browser. Only then does a workspace exist, and only then can a key be
      minted. An agent never handles a payment credential and cannot complete a
      checkout on anyone's behalf.


      **Throttled instead of captcha-gated.** A captcha proves a human is
      present, which is the opposite of this endpoint's premise. `POST /signup`
      is capped per client IP and per email address instead. Exceeding either
      returns `429` with a `Retry-After`; the limiter failing returns `503`, and
      never lets a request through.


      **No account-existence oracle.** An accepted `POST /signup` always answers
      `{"status": "verification_sent"}`, whether or not the address already has
      a Datalyr account. Do not infer anything about the address from the
      response.


      **`GET /onboarding` is the checklist.** It needs an Agent key, so it only
      works once a workspace exists — that is, after payment. Read `next_steps`
      and drive the human through them; re-read it to confirm each one landed.
  - name: Documentation
    description: >-
      Search the published Datalyr documentation. The only routes here that read
      content outside your workspace.
  - name: Exports
    description: >-
      Ask for a file instead of a page. An export runs asynchronously and
      produces a CSV or NDJSON object you download with a temporary link.


      **It is a job, not a response.** `POST /exports` returns `202` with a job
      id and no data — an export can scan a year of events and take minutes,
      which is longer than any HTTP request should live. Poll `GET
      /exports/{exportId}` until `status` is `completed`, then read
      `download.url`.


      **Scope.** `datalyr:read`, the same scope every other read on this page
      needs — including the original per-workspace Agent key. An export returns
      rows you can already fetch through `GET /events` or `GET /users`; it does
      not change anything you configured, so it does not require a write scope.


      **The download link is temporary and so is the file.** `download.url` is a
      signed URL valid for one hour from the moment you polled; poll again for a
      fresh one. The file itself is deleted seven days after the export was
      created (`expires_at`), after which the job reads `expired` and must be
      re-run. Treat the URL as a credential: anyone holding it can download the
      file until it expires.


      **Limits.** One export covers at most 366 days and one million rows (users
      exports: 100,000, the bound of the person-grain query). Hitting the row
      cap sets `truncated: true` on the finished job rather than failing it —
      you get the rows that fit and are told the range is not fully covered. A
      workspace may have five exports queued or running at once.


      **What the files contain.** `events` mirrors `GET /events` column for
      column; `users` mirrors `GET /users`; `attribution` is the channel-grain
      summary from `GET /attribution`, one row per channel plus an
      `unattributed` row. Filters are an allowlist per type — an unrecognized
      filter is rejected rather than ignored, so you never receive more rows
      than you asked for without being told.
  - name: Write API
    description: >-
      Create, change and delete conversion rules and trackable links, reverse
      any of it, and propose container scripts for a human to approve.


      **Two tiers, and the difference is deliberate.** Conversion rules and
      trackable links are direct writes. Container scripts are **propose-only**:
      `POST /scripts/proposals` records a draft and nothing else. No scope
      publishes a script, and there is no endpoint that approves one — an owner
      or admin approves it in the Datalyr dashboard, and only then does the
      script exist. A container script is arbitrary JavaScript injected into
      every page of the customer's site, so a key that could publish one would
      turn a leaked key into a supply-chain compromise rather than a data-access
      problem.


      **Scopes.** These routes need a named API key carrying an explicit write
      scope: `datalyr:write:rules` for conversion rules, `datalyr:write:links`
      for trackable links, `datalyr:propose:scripts` to draft a container
      script. The original per-workspace Agent key does not have them and never
      will — it resolves to `datalyr:read` alone, so it reads every route in
      this spec and writes none of them. Mint a named key with the scopes you
      want; a request without the right one returns `403` with `code:
      insufficient_scope` and the `required_scope` it wanted.


      **Dry run.** Every mutation accepts `dry_run`. A dry run validates the
      request, reads whatever it needs to describe the outcome, and returns the
      exact change it would make — including, for a conversion rule, which
      platform, ad account and pixel the rule would send conversions to. It
      writes nothing: no row, no audit entry, no edge redirect, no idempotency
      record. It defaults to `false` over raw HTTP, because a `DELETE` you sent
      is a `DELETE` you meant. The MCP write tools default it to `true`.


      **Idempotency.** `POST` accepts an `Idempotency-Key` header. The first
      request with a given key is performed and its response kept for 24 hours;
      an identical retry returns that same response with `Idempotent-Replay:
      true` rather than creating a second resource. The record covers the
      request body too, so reusing a key with different content is a new
      request, not a replay. `PATCH` and `DELETE` are keyed on a resource id and
      need no header.


      **Audit and undo.** Every applied write records what changed, who changed
      it and the state it replaced, in the same transaction as the change. `GET
      /audit` lists those entries and `POST /audit/{auditId}/undo` puts the
      resource back. Undo refuses rather than guessing: it will not reverse a
      create (call `DELETE`, which previews and audits like any other write),
      will not run twice on one entry, and will not restore over a resource that
      has since been deleted or re-created.


      **Rate limit.** Mutations spend a separate, tighter budget than reads: 20
      per minute per key, including dry runs.
paths:
  /events/properties:
    get:
      tags:
        - Events and users
      summary: List event properties
      description: >-
        The catalog of custom `event_data` property keys this workspace sends,
        grouped by event name, each with how often it appeared, how many
        distinct values it took, one sample value, and its JSON type. Use it
        before `property_filters` on `/events`.


        The counts come from a bounded SAMPLE of the most recent events in the
        window, not from the whole window: they rank keys, they do not measure
        them, and a low-volume event's keys can be missed entirely. Pass
        `event_name` to spend the whole sample on one event, which is the
        reliable way to enumerate a rare event's properties.


        Top-level keys only. A nested object appears once with `value_type`
        `object`; its inner keys are not listed, and `/events` cannot filter on
        them either.
      operationId: list-event-properties
      parameters:
        - name: event_name
          in: query
          description: Restrict the sample to one event name. Omit to sample every event.
          schema:
            type: string
            maxLength: 200
        - name: lookback_days
          in: query
          description: >-
            How many days back to sample. The maximum equals the source's 90-day
            retention.
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 7
        - name: limit
          in: query
          description: Maximum catalog rows returned, most frequent first.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 200
      responses:
        '200':
          $ref: '#/components/responses/EventProperties'
        '400':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
components:
  responses:
    EventProperties:
      description: >-
        Successful response. Sampled: counts rank property keys, they do not
        measure them.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EventPropertiesResponse'
    Error:
      description: Error response.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
  schemas:
    EventPropertiesResponse:
      type: object
      additionalProperties: true
      required:
        - properties
        - returned_count
        - complete
      properties:
        properties:
          type: array
          description: >-
            Most frequent first. Empty only when no sampled event carried any
            property.
          items:
            type: object
            additionalProperties: true
            properties:
              event_name:
                type: string
              property_key:
                type: string
                description: Use verbatim as a `property_filters` key on `/events`.
              occurrences:
                type: integer
                description: >-
                  Sampled events carrying the key. A rank, NOT a workspace total
                  — the sample is bounded and recent-biased.
              distinct_values:
                type: integer
                description: >-
                  Distinct values within the sample. Low means the key is
                  enum-like (`plan`, `currency`); high means it identifies a row
                  (`order_id`).
              sample_value:
                type: string
                description: >-
                  One observed value, quote-stripped and truncated to 200
                  characters. An illustration of the shape, not the most common
                  value.
              value_type:
                type: string
                enum:
                  - string
                  - number
                  - boolean
                  - 'null'
                  - object
                  - array
                description: >-
                  JSON type of the sampled value. `gt` and `lt` are only
                  meaningful on `number`; an `object` cannot be filtered into at
                  all.
        returned_count:
          type: integer
        complete:
          type: boolean
          description: >-
            Always `true` on a 200. A failed read is a `503`, never an empty
            catalog.
        meta:
          type: object
          additionalProperties: true
          properties:
            lookback_days:
              type: integer
            sample_size:
              type: integer
              description: Maximum events read. The scan bound, not a result limit.
            event_name:
              type: string
              description: Present only when the sample was narrowed to one event.
            limit:
              type: integer
            value_types:
              type: array
              items:
                type: string
            filterable_ops:
              type: array
              items:
                type: string
              description: The `op` values `/events` `property_filters` accepts.
            limitation:
              type: string
              description: States the sampled and top-level-keys-only scope in prose.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Datalyr Agent key (dk_agent_…)

````