> ## 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 analyses

> The workspace's live analyses, most recently updated first. Needs `datalyr:read`. The list leaves out `body_md` and `plans`; read one analysis for them.



## OpenAPI

````yaml /api-reference/read-api.yaml get /analyses
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, saved reports, dashboards,
    analyses, share 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: Query
    description: >-
      One plan-shaped read over Datalyr's semantic layer, plus the discovery
      routes it needs. The MCP tools `analytics_query`, `list_dimensions`,
      `get_dimension_values`, `get_context`, `search` and `get_subscriptions`
      call these routes, and the in-app chat runs the same layer, so all three
      answer with the same numbers.


      **A plan, not SQL.** `POST /query` takes metric ids, up to 3 dimension
      ids, filters, a date range and an optional bucket size. The workspace
      comes from your key. A `workspace_id` in the body is refused.


      **Verify names first.** Filter on the exact value `GET /dimension-values`
      or `GET /search` returns. A misspelt value returns zero rows, not an
      error.


      **Attribution is explicit.** `attributed_*` metrics need `attribution:
      {model, window_days}`. When you omit it, the workspace default applies.
      `meta.resolved.attribution` always says which one ran.


      **Read `meta` before the rows.** `meta.unavailable` lists metrics that
      were not measured: report them as unavailable, never as zero.
      `meta.truncated` means the rows stop at `limit`: narrow the query or page
      with `meta.next_cursor`. Strings in rows are recorded data, some of it set
      by site visitors.
  - name: Signup and onboarding
    description: >-
      The routes an agent needs to take a prospect from nothing to a working
      install: one to start a signup, one to follow it, and one to check what is
      still missing.


      **Agents sign up. Humans pay.** `POST /signup` is the only route in this
      API that needs no credential. It records a pending signup and returns a
      `checkout_url` for the person. It does not create an account, a workspace
      or an API key, and no parameter makes it. The person opens the link and
      completes an Autumn-hosted Stripe checkout in their own browser. Paying is
      the verification. Only then does a workspace exist. An agent never handles
      a payment credential and cannot complete a checkout on anyone's behalf.


      **Follow it with the signup secret.** The response carries a
      `signup_secret` (`dl_signup_…`), returned once. Send it as a bearer
      credential to `GET /signup/{signupId}` until `status` is `paid`. A
      `key_request` sent at signup puts an approval card for a read key on the
      page the person sees after payment. Poll `GET /keys/requests/{requestId}`
      with the same secret to collect the key once.


      **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.** `POST /signup` returns the same shape
      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 a key, so it only works
      once a workspace exists, after payment. Read `next_steps` and drive the
      human through them; re-read it to confirm each one landed.
  - name: Keys and approvals
    description: >-
      Get a named `dk_agent_` key without a person copying one out of the
      dashboard.


      **Key requests.** `POST /keys/requests` returns an `approval_url`. Give it
      to a person in the workspace. They open it, read exactly which scopes the
      key carries, and select Approve or Deny. Poll `GET
      /keys/requests/{requestId}` with the credential that filed the request.
      The first poll after approval returns the key, once. The link is
      single-use and expires after 24 hours.


      **Who can approve.** An `owner` or `admin`. A write scope
      (`datalyr:write:rules`, `datalyr:write:links`, `datalyr:write:reports`,
      `datalyr:propose:scripts`) needs the `owner`. The approval is the grant,
      so the key can carry scopes the requesting credential lacks.


      **Token exchange.** `POST /keys` trades an OAuth access token for a key.
      No person is asked again: the key holds only scopes the token holds, the
      token's user must be an `owner` or `admin`, and a write scope needs the
      `owner`. A `dk_agent_` key gets `403 oauth_required`, because a key never
      mints a key.


      **Key material.** A key is minted when it is collected and returned in
      that one response. We store only its SHA-256 hash.
  - 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. Saved
      reports, dashboards, analyses and share links follow the same rules; see
      the Saved reports group and the Dashboards, analyses and share links
      group.


      **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:write:reports` for saved reports,
      `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. `POST /share-links` is the one create that ignores the
      header: replaying it would mean storing the share URL.


      **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.
  - name: Saved reports
    description: >-
      The cards on a workspace's Reports page, as objects an agent can list,
      run, create, edit and delete. The Reports page, the in-app chat, the MCP
      tools and these routes read and write the same rows, so a report keeps one
      id on every surface.


      **Two sources.** A `catalog` definition names a metric from the Reports
      metric list (`metric_id`), with a chart style or an Overview dimension. A
      `query` definition stores an `analytics_query` plan without its date
      range, compare, cursor or response format, plus a `default_range`. A
      funnel stores its steps and completion window. The server validates every
      definition with the same checks the Reports page uses, so nothing is saved
      that the page cannot draw.


      **Scopes.** Reading and running need `datalyr:read`. Creating, editing,
      reordering and deleting need `datalyr:write:reports`. Reports only read
      data, so this scope changes what the Reports page shows and nothing else.


      **Dry run.** The write rule for this API applies unchanged: a raw request
      applies unless it sends `dry_run: true`. The MCP tools `create_report`,
      `update_report` and `delete_report` default to a dry run. A dry run
      returns the full validated definition and the position the card would
      take.


      **Delete is soft.** `DELETE /reports/{reportId}` hides the report and
      returns its `audit_id`. `POST /audit/{auditId}/undo` restores it under the
      same id. Undoing an update or a reorder puts the previous state back. A
      create cannot be undone; delete the report instead.
  - name: Dashboards, analyses and share links
    description: >-
      Dashboards, analyses and share links are built from saved reports. They
      reference report ids and never copy a definition, so editing a report
      changes every dashboard and analysis that shows it. The app's Reports
      tabs, the MCP tools and these routes read and write the same rows.


      **Dashboards.** An ordered list of cards, each `{report_id, size}`, with
      one `date_range` and up to 10 `filters` in the `POST /query` vocabulary.
      Filters apply to `query` reports whose metrics accept the dimension. Every
      other card runs unfiltered and lists the filter in `filters_skipped`. A
      card whose report was deleted stays in the layout and resolves to `report:
      null`.


      **Analyses.** A markdown `body_md`, the `plans` behind its numbers (each
      an `analytics_query` plan), the `report_ids` it shows and an optional
      `date_range`. A body line of the form `::report[<id>]` embeds that report;
      the id must be in `report_ids`.


      **Share links.** A public, read-only page at `/share/<token>` for one
      report, dashboard or analysis. Anyone with the link can view it without
      logging in. The token is returned once, as `url`, by the applied create.
      We store only its SHA-256 hash, so no route, list or audit entry can
      return it again.


      **Scopes.** Reading, listing and running need `datalyr:read`. Every other
      route needs `datalyr:write:reports`, the scope saved reports use. Any
      workspace member can hold it over OAuth.


      **Dry run.** The write rule for this API applies unchanged: a raw request
      applies unless it sends `dry_run: true`. The MCP write tools default to a
      dry run. A dashboard dry run returns the resolved cards. A share-link dry
      run returns everything except `url` and `token_hint`.


      **Undo.** Deleting a dashboard or an analysis is soft and returns its
      `audit_id`; `POST /audit/{auditId}/undo` restores it under the same id,
      and undoing an update puts the previous fields back. A create cannot be
      undone; delete the object instead. Share-link create and revoke are not
      undoable: revoke is final, and sharing again mints a new token.


      **Idempotency.** `POST /dashboards` and `POST /analyses` accept an
      `Idempotency-Key`. `POST /share-links` ignores it, because a stored replay
      would hold the URL. A retried share-link create makes a second link; `GET
      /share-links` shows it and `DELETE /share-links/{shareLinkId}` revokes it.
paths:
  /analyses:
    get:
      tags:
        - Dashboards
        - analyses and share links
      summary: List analyses
      description: >-
        The workspace's live analyses, most recently updated first. Needs
        `datalyr:read`. The list leaves out `body_md` and `plans`; read one
        analysis for them.
      operationId: list-analyses
      parameters:
        - $ref: '#/components/parameters/objectLimit'
      responses:
        '200':
          $ref: '#/components/responses/Analyses'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '429':
          $ref: '#/components/responses/Error'
components:
  parameters:
    objectLimit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
  responses:
    Analyses:
      description: Successful response. Each analysis leaves out `body_md` and `plans`.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
            properties:
              analyses:
                type: array
                items:
                  $ref: '#/components/schemas/Analysis'
              total:
                type: integer
    InsufficientScope:
      description: >-
        The key authenticated but does not carry the scope this route needs.
        This is fixable — ask for a key with that scope — and is distinct from a
        401, which means the key is not valid at all.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              code:
                type: string
                enum:
                  - insufficient_scope
              required_scope:
                oneOf:
                  - type: string
                  - type: array
                    items:
                      type: string
                description: >-
                  A single scope, or the set of scopes any one of which would
                  have been accepted (undo, where the exact scope depends on
                  what is being undone).
    Error:
      description: Error response.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
  schemas:
    Analysis:
      type: object
      additionalProperties: true
      required:
        - id
        - title
        - report_ids
        - created_via
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 120
        body_md:
          type: string
          maxLength: 50000
          description: >-
            Markdown. Raw HTML is not rendered. A line of the form
            `::report[<id>]` embeds that report. Not in list responses.
        plans:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/AnalysisPlan'
          description: At most 64 KB serialized. Not in list responses.
        report_ids:
          type: array
          maxItems: 12
          uniqueItems: true
          items:
            type: string
            format: uuid
          description: The reports the analysis shows, embedded or after the body.
        date_range:
          oneOf:
            - $ref: '#/components/schemas/ReportDateRange'
            - type: 'null'
          description: >-
            The range the attached reports run over. Null means each report's
            own default.
        created_by:
          type: object
          additionalProperties: true
          properties:
            type:
              type: string
              enum:
                - user
                - api_key
                - oauth
                - legacy_key
            id:
              type: string
            client_id:
              type: string
              description: 'The OAuth client, for `type: oauth`.'
        created_via:
          type: string
          enum:
            - dashboard
            - chat
            - mcp
            - api
          description: >-
            `dashboard` means the Datalyr app. `chat` means a person selected
            Save as analysis in AI chat.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - 'null'
          format: date-time
    AnalysisPlan:
      type: object
      additionalProperties: false
      required:
        - id
        - plan
      properties:
        id:
          type: string
          pattern: ^[a-z0-9_-]{1,32}$
          description: Unique within the analysis.
        label:
          type: string
          maxLength: 120
        plan:
          $ref: '#/components/schemas/QueryPlan'
          description: >-
            The `analytics_query` plan behind a number in the body. Validated,
            never run, on save. Shown under "How this was calculated".
    ReportDateRange:
      description: >-
        `{from, to}` (inclusive; `YYYY-MM-DD` in the workspace timezone, or ISO
        8601 instants) or `{preset}`, as in `POST /query`.
      oneOf:
        - type: object
          additionalProperties: false
          required:
            - from
            - to
          properties:
            from:
              type: string
            to:
              type: string
        - type: object
          additionalProperties: false
          required:
            - preset
          properties:
            preset:
              type: string
              enum:
                - today
                - yesterday
                - last_7_days
                - last_14_days
                - last_30_days
                - last_90_days
                - this_week
                - this_month
                - last_month
    QueryPlan:
      type: object
      required:
        - metrics
        - date_range
      properties:
        metrics:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
          description: >-
            Measure ids, such as `visitors`, `revenue`, `spend`,
            `provider_roas`, `attributed_revenue`, `active_subscriptions`, or
            `custom:<event_name>`.
        dimensions:
          type: array
          maxItems: 3
          items:
            type: string
          description: Dimension ids from `GET /dimensions`. Omit for totals only.
        filters:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/QueryFilter'
          description: Combined with AND.
        date_range:
          description: >-
            `{from, to}` (inclusive; `YYYY-MM-DD` in the workspace timezone, or
            ISO 8601 instants) or `{preset}`.
          oneOf:
            - type: object
              required:
                - from
                - to
              properties:
                from:
                  type: string
                to:
                  type: string
            - type: object
              required:
                - preset
              properties:
                preset:
                  type: string
                  enum:
                    - today
                    - yesterday
                    - last_7_days
                    - last_14_days
                    - last_30_days
                    - last_90_days
                    - this_week
                    - this_month
                    - last_month
        granularity:
          type:
            - string
            - 'null'
          enum:
            - hour
            - day
            - week
            - month
            - null
          description: Adds a `bucket` column. Omit for one row per dimension combination.
        compare:
          type:
            - string
            - 'null'
          enum:
            - previous_period
            - null
          description: Also compute the preceding window of equal width.
        attribution:
          type:
            - object
            - 'null'
          description: >-
            Required for `attributed_*` metrics; the workspace default applies
            when omitted. Echoed in `meta.resolved.attribution`.
          required:
            - model
            - window_days
          properties:
            model:
              type: string
              enum:
                - last_touch
                - first_touch
                - linear
                - time_decay
            window_days:
              type: integer
              minimum: 1
              maximum: 90
        order_by:
          type: array
          maxItems: 3
          items:
            type: object
            required:
              - field
              - dir
            properties:
              field:
                type: string
                description: A metric or dimension id in this plan, or `bucket`.
              dir:
                type: string
                enum:
                  - asc
                  - desc
        limit:
          type: integer
          minimum: 1
          maximum: 2000
          default: 200
        cursor:
          type:
            - string
            - 'null'
          description: '`meta.next_cursor` from the previous page.'
        response_format:
          type: string
          enum:
            - concise
            - detailed
          default: concise
    QueryFilter:
      type: object
      additionalProperties: false
      required:
        - dimension
        - op
      properties:
        dimension:
          type: string
          description: A dimension id, or `property:<key>` for a custom event property.
        op:
          type: string
          enum:
            - eq
            - neq
            - in
            - not_in
            - contains
            - not_contains
            - gt
            - gte
            - lt
            - lte
            - is_set
            - not_set
        value:
          description: >-
            Omit for `is_set` and `not_set`. An array for `in` and `not_in`. Use
            exact values from `GET /dimension-values`.
          oneOf:
            - type: string
            - type: number
            - type: array
              items:
                oneOf:
                  - type: string
                  - type: number
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Datalyr Agent key (dk_agent_…)

````