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

# Start an export

> Queue a CSV or NDJSON export and return immediately.

The `202` carries the job id and its `queued` status — never the data. Poll `GET /exports/{exportId}`, or the `poll_url` on the response, until `status` is `completed`, then download `download.url` within the hour.

A `400` means the request is wrong and retrying it unchanged will fail the same way: an unknown type or format, a missing or reversed date range, a range over 366 days, or a filter this export type does not support. A `429` with `code: too_many_exports` means five jobs are already queued or running for this workspace; wait for one to finish. A `503` means the export service could not accept the job — nothing was created, so retrying is safe.



## OpenAPI

````yaml /api-reference/read-api.yaml post /exports
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:
  /exports:
    post:
      tags:
        - Exports
      summary: Start an export
      description: >-
        Queue a CSV or NDJSON export and return immediately.


        The `202` carries the job id and its `queued` status — never the data.
        Poll `GET /exports/{exportId}`, or the `poll_url` on the response, until
        `status` is `completed`, then download `download.url` within the hour.


        A `400` means the request is wrong and retrying it unchanged will fail
        the same way: an unknown type or format, a missing or reversed date
        range, a range over 366 days, or a filter this export type does not
        support. A `429` with `code: too_many_exports` means five jobs are
        already queued or running for this workspace; wait for one to finish. A
        `503` means the export service could not accept the job — nothing was
        created, so retrying is safe.
      operationId: create-export
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExportRequest'
      responses:
        '202':
          $ref: '#/components/responses/ExportAccepted'
        '400':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
components:
  schemas:
    CreateExportRequest:
      type: object
      additionalProperties: false
      required:
        - type
        - params
      properties:
        type:
          type: string
          enum:
            - events
            - users
            - attribution
          description: >-
            `events` mirrors `GET /events`; `users` mirrors `GET /users` at
            person grain; `attribution` is the channel-grain summary from `GET
            /attribution`.
        format:
          type: string
          enum:
            - csv
            - ndjson
          default: csv
          description: >-
            `ndjson` is one JSON object per line, keyed by the same columns as
            the CSV.
        params:
          type: object
          additionalProperties: false
          required:
            - start_date
            - end_date
          properties:
            start_date:
              type: string
              description: ISO 8601 instant or date.
            end_date:
              type: string
              description: >-
                ISO 8601 instant or date. Must be after `start_date`, by at most
                366 days.
            filters:
              type: object
              additionalProperties: true
              description: >-
                Optional, and an allowlist per type — an unrecognized key is
                rejected, never ignored.


                `events`: `event_name` (comma-separated, up to 50),
                `utm_source`, `utm_campaign`, `country`, `page_path`. `users`:
                `search`. `attribution`: `model` (`last_touch`, `first_touch`,
                `linear`, `time_decay`), `window_days` (1-365),
                `conversion_events` (comma-separated, up to 8).
    ExportAcceptedResponse:
      allOf:
        - $ref: '#/components/schemas/ExportJob'
        - type: object
          properties:
            poll_url:
              type: string
              description: Where to poll for this job.
            limits:
              type: object
              additionalProperties: true
              properties:
                max_rows:
                  type: integer
    ExportJob:
      type: object
      additionalProperties: true
      required:
        - id
        - status
        - type
        - format
        - created_at
        - expires_at
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - queued
            - running
            - completed
            - failed
            - expired
          description: >-
            Only `completed` carries a download. `expired` means `expires_at`
            has passed and the file has been deleted.
        type:
          type: string
          enum:
            - events
            - users
            - attribution
        format:
          type: string
          enum:
            - csv
            - ndjson
        params:
          type: object
          additionalProperties: true
          description: The range and filters this export was created with.
          properties:
            start_date:
              type:
                - string
                - 'null'
            end_date:
              type:
                - string
                - 'null'
            filters:
              type: object
              additionalProperties: true
        row_count:
          type:
            - integer
            - 'null'
          description: Rows written. Null until the export finishes.
        truncated:
          type: boolean
          description: >-
            True when the export reached the row cap. The file is valid but does
            not cover the whole requested range; narrow the range or the
            filters.
        error:
          type:
            - string
            - 'null'
          description: Why it failed. Null unless `status` is `failed`.
        created_at:
          type: string
        started_at:
          type:
            - string
            - 'null'
        completed_at:
          type:
            - string
            - 'null'
        expires_at:
          type: string
          description: >-
            When the file is deleted — seven days after creation. After this the
            export must be re-run.
        download:
          oneOf:
            - $ref: '#/components/schemas/ExportDownload'
            - type: 'null'
          description: >-
            Present only on a completed, unexpired job. Generated at poll time,
            never stored. `null` alongside `download_error` means the artifact
            exists but could not be signed — retry the poll.
        download_error:
          type: string
          description: Set only when `download` is null on a completed job.
    ExportDownload:
      type: object
      additionalProperties: true
      required:
        - url
        - expires_at
      properties:
        url:
          type: string
          description: >-
            Signed download link. Treat it as a credential: anyone holding it
            can fetch the file until it expires.
        expires_in:
          type: integer
          description: Seconds the URL remains valid. At most 3600.
        expires_at:
          type: string
          description: When this URL stops working — not when the file is deleted.
  responses:
    ExportAccepted:
      description: >-
        Accepted. The export is queued; poll `poll_url` for its status. The
        `Location` header carries the same path.
      headers:
        Location:
          description: Where to poll for this job.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ExportAcceptedResponse'
    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_…)

````