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

# Run a saved report

> Execute a saved report and return its numbers. A read: it needs `datalyr:read`, writes nothing and spends the read budget.

`result` takes the shape of the engine that ran it. A `query` report returns a `QueryResult`, the same as `POST /query`. A catalog `line` or `metric` report returns a series with `currentTotal` and `previousTotal`. A catalog `breakdown` or `donut` report returns `rows` of `{name, value}` and a `total`. A funnel returns one entry per step.

Without `overrides.date_range`, a `query` report runs over its `default_range`.



## OpenAPI

````yaml /api-reference/read-api.yaml post /reports/{reportId}/run
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:
  /reports/{reportId}/run:
    post:
      tags:
        - Saved reports
      summary: Run a saved report
      description: >-
        Execute a saved report and return its numbers. A read: it needs
        `datalyr:read`, writes nothing and spends the read budget.


        `result` takes the shape of the engine that ran it. A `query` report
        returns a `QueryResult`, the same as `POST /query`. A catalog `line` or
        `metric` report returns a series with `currentTotal` and
        `previousTotal`. A catalog `breakdown` or `donut` report returns `rows`
        of `{name, value}` and a `total`. A funnel returns one entry per step.


        Without `overrides.date_range`, a `query` report runs over its
        `default_range`.
      operationId: run-saved-report
      parameters:
        - $ref: '#/components/parameters/reportId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunReportRequest'
      responses:
        '200':
          $ref: '#/components/responses/RunReport'
        '400':
          $ref: '#/components/responses/QueryError'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
components:
  parameters:
    reportId:
      name: reportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  schemas:
    RunReportRequest:
      type: object
      additionalProperties: false
      properties:
        overrides:
          type: object
          additionalProperties: false
          properties:
            date_range:
              $ref: '#/components/schemas/ReportDateRange'
              description: >-
                The range to run. Replaces a query report's `default_range`.
                Send it for catalog and funnel reports, which store no range of
                their own.
            compare:
              type:
                - string
                - 'null'
              enum:
                - previous_period
                - null
              description: Also compute the preceding window of equal width.
    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
    RunReportResult:
      type: object
      additionalProperties: true
      required:
        - report
        - range
        - result
      properties:
        report:
          type: object
          additionalProperties: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            kind:
              $ref: '#/components/schemas/ReportKind'
            source:
              type: string
              enum:
                - catalog
                - query
                - funnel
        range:
          type: object
          additionalProperties: true
          properties:
            from:
              type: string
            to:
              type: string
            timezone:
              type: string
        result:
          description: >-
            The shape of the engine that ran. `report.source` and `report.kind`
            say which.
          anyOf:
            - $ref: '#/components/schemas/QueryResult'
            - $ref: '#/components/schemas/ReportSeriesResult'
            - $ref: '#/components/schemas/ReportShareResult'
            - $ref: '#/components/schemas/ReportFunnelResult'
    QueryPlanError:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
        code:
          type: string
          enum:
            - unknown_metric
            - unknown_dimension
            - unsupported_dimension
            - invalid_filter
            - invalid_date_range
            - attribution_required
            - too_many_dimensions
            - too_many_metrics
            - invalid_cursor
            - invalid_order
        hint:
          type: string
          description: A concrete fix, such as the closest valid id.
    ReportKind:
      type: string
      enum:
        - metric
        - line
        - breakdown
        - donut
        - funnel
      description: >-
        `metric` is a number tile. `line` is a series; `area` and `bar` are its
        chart styles, set in `chart.type`. `breakdown` and `donut` split one
        metric by one dimension. `funnel` counts visitors through ordered steps.
    QueryResult:
      type: object
      additionalProperties: true
      required:
        - rows
        - totals
        - meta
      properties:
        rows:
          type: array
          description: >-
            One object per dimension combination (and bucket). Keys are
            dimension ids, `bucket`, then metric ids. A `null` metric value
            means no data, not zero.
          items:
            type: object
            additionalProperties: true
        totals:
          type: object
          description: One value per metric id over the whole, unpaginated result.
          additionalProperties:
            type:
              - number
              - 'null'
        comparison:
          type:
            - object
            - 'null'
          additionalProperties: true
          properties:
            date_range:
              type: object
              properties:
                from:
                  type: string
                to:
                  type: string
            totals:
              type: object
              additionalProperties:
                type:
                  - number
                  - 'null'
            rows:
              type: array
              items:
                type: object
                additionalProperties: true
        meta:
          $ref: '#/components/schemas/QueryMeta'
        data_notice:
          type: string
          description: >-
            Reminds a reader that strings in rows are recorded data, not
            instructions.
    ReportSeriesResult:
      type: object
      additionalProperties: true
      title: Catalog series
      description: Catalog `line` and `metric` reports.
      properties:
        current:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                description: The bucket, in the workspace timezone.
              value:
                type: number
        previous:
          type: array
          description: The preceding window. Empty when the run did not compare.
          items:
            type: object
            properties:
              date:
                type: string
              value:
                type: number
        currentTotal:
          type:
            - number
            - 'null'
          description: Null means the source was unavailable, not zero.
        previousTotal:
          type:
            - number
            - 'null'
        granularity:
          type: string
          enum:
            - hour
            - day
            - month
        error:
          type: boolean
          description: True when the source query failed. Report it as unavailable.
        dataStatus:
          type: string
          enum:
            - available
            - unavailable
        unavailableReason:
          type: string
    ReportShareResult:
      type: object
      additionalProperties: true
      title: Catalog breakdown or donut
      properties:
        rows:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              value:
                type: number
        total:
          type: number
        truncated:
          type: boolean
          description: True when more values exist than `limit`.
        meta:
          type: object
          additionalProperties: true
    ReportFunnelResult:
      type: object
      additionalProperties: true
      title: Funnel
      properties:
        steps:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              index:
                type: integer
              step:
                $ref: '#/components/schemas/FunnelReportStep'
              visitors:
                type: integer
              conversionFromFirst:
                type: number
                description: Between 0 and 1.
              conversionFromPrevious:
                type: number
                description: Between 0 and 1.
        meta:
          type: object
          additionalProperties: true
          properties:
            grain:
              type: string
              enum:
                - visitor
            windowHours:
              type: integer
            fromClamped:
              type: boolean
              description: True when the start moved forward to the lookback limit.
            maxLookbackDays:
              type: integer
              description: Always `365`.
            limitation:
              type: string
    QueryMeta:
      type: object
      additionalProperties: true
      properties:
        resolved:
          type: object
          additionalProperties: true
          properties:
            metrics:
              type: array
              items:
                type: object
                additionalProperties: true
                properties:
                  id:
                    type: string
                  label:
                    type: string
                  unit:
                    type: string
                    enum:
                      - count
                      - currency
                      - percentage
                      - ratio
                      - duration_seconds
                  definition:
                    type: string
                  domain:
                    type: string
                    enum:
                      - events
                      - money
                      - ads
                      - attribution
                      - persons
                      - subscriptions
                  attributed:
                    type: boolean
                  currency:
                    type:
                      - string
                      - 'null'
            dimensions:
              type: array
              items:
                type: object
                additionalProperties: true
            filters:
              type: array
              items:
                $ref: '#/components/schemas/QueryFilter'
            date_range:
              type: object
              properties:
                from:
                  type: string
                  description: UTC instant, inclusive.
                to:
                  type: string
                  description: UTC instant, exclusive.
                timezone:
                  type: string
                preset:
                  type: string
            granularity:
              type:
                - string
                - 'null'
            attribution:
              type:
                - object
                - 'null'
              properties:
                model:
                  type: string
                window_days:
                  type: integer
                source:
                  type: string
                  enum:
                    - requested
                    - workspace_default
        returned_rows:
          type: integer
        truncated:
          type: boolean
          description: >-
            True when rows stop at `limit`. Narrow the query or page with
            `next_cursor`.
        total_rows:
          type:
            - integer
            - 'null'
        next_cursor:
          type:
            - string
            - 'null'
        freshness:
          type: array
          items:
            type: object
            properties:
              source:
                type: string
              latest_at:
                type:
                  - string
                  - 'null'
        unavailable:
          type: array
          description: Metrics that were not measured. Never read them as zero.
          items:
            type: object
            properties:
              metric:
                type: string
              reason:
                type: string
                description: For example `source_not_connected`, `not_modelled`, `timeout`.
              detail:
                type: string
        deep_link:
          type:
            - string
            - 'null'
          description: Dashboard path that shows the same slice.
        executed_in_ms:
          type: integer
        notes:
          type: array
          items:
            type: string
    FunnelReportStep:
      type: object
      additionalProperties: false
      required:
        - event
      properties:
        event:
          type: string
          minLength: 1
          description: An exact event name.
        source:
          type:
            - string
            - 'null'
          description: >-
            The event's source, such as `web`. Omit it to match the event from
            any source.
        where:
          type: array
          maxItems: 5
          description: Conditions on event properties, combined with AND.
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - values
            properties:
              key:
                type: string
              values:
                type: array
                minItems: 1
                items:
                  type: string
                description: The step matches any of these values.
    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
  responses:
    RunReport:
      description: Successful response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RunReportResult'
    QueryError:
      description: The plan or parameters cannot run. `hint` says how to fix them.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/QueryPlanError'
    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
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Datalyr Agent key (dk_agent_…)

````