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

# Get retention cohorts

> Person-grain retention cohorts. Each person is anchored to the cohort of their ALL-TIME first recorded activity, so `start_date` and `end_date` select which cohorts come back, not what counts as acquisition. Every cohort carries one row per elapsed period with the persons active in it, the retention rate, and the revenue those persons recorded.

Activity retention only: a person counts as retained for ANY activity in the period. Event-specific retention, such as returned and purchased, is not available in this version and is not approximated.



## OpenAPI

````yaml /api-reference/read-api.yaml get /retention
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:
  /retention:
    get:
      tags:
        - Events and users
      summary: Get retention cohorts
      description: >-
        Person-grain retention cohorts. Each person is anchored to the cohort of
        their ALL-TIME first recorded activity, so `start_date` and `end_date`
        select which cohorts come back, not what counts as acquisition. Every
        cohort carries one row per elapsed period with the persons active in it,
        the retention rate, and the revenue those persons recorded.


        Activity retention only: a person counts as retained for ANY activity in
        the period. Event-specific retention, such as returned and purchased, is
        not available in this version and is not approximated.
      operationId: get-retention
      parameters:
        - name: start_date
          in: query
          description: >-
            Inclusive first cohort date, as `YYYY-MM-DD` or an ISO 8601 instant.
            Supply with `end_date`; omit both for the last 90 days of cohorts.
          schema:
            type: string
        - name: end_date
          in: query
          description: Inclusive last cohort date, as `YYYY-MM-DD` or an ISO 8601 instant.
          schema:
            type: string
        - name: granularity
          in: query
          description: Cohort bucket and period unit. Weeks start Monday.
          schema:
            type: string
            enum:
              - day
              - week
              - month
            default: week
        - name: max_periods
          in: query
          description: >-
            Highest period index returned, counted in `granularity` units from
            period 0.
          schema:
            type: integer
            minimum: 1
            maximum: 24
            default: 12
      responses:
        '200':
          $ref: '#/components/responses/Retention'
        '400':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
components:
  responses:
    Retention:
      description: >-
        Successful response. Activity retention: a person counts as retained for
        any activity in the period, not for a specific event.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RetentionResponse'
    Error:
      description: Error response.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
  schemas:
    RetentionResponse:
      type: object
      additionalProperties: true
      required:
        - cohorts
        - granularity
        - date_range
        - complete
      properties:
        cohorts:
          type: array
          description: >-
            One entry per cohort, oldest first. Empty when no cohort falls in
            the range.
          items:
            type: object
            additionalProperties: true
            required:
              - cohort_date
              - cohort_size
              - periods
            properties:
              cohort_date:
                type: string
                description: Bucket start, `YYYY-MM-DD`. Monday for weekly cohorts.
              cohort_size:
                type: integer
                description: >-
                  Distinct persons whose all-time first activity falls in this
                  bucket.
              periods:
                type: array
                description: >-
                  Ascending by `period_index`. A period with no activity is
                  absent, not zero-filled.
                items:
                  type: object
                  additionalProperties: true
                  properties:
                    period_index:
                      type: integer
                      description: >-
                        Elapsed `granularity` units since the cohort bucket. 0
                        is the acquisition period.
                    active_persons:
                      type: integer
                      description: Cohort members with any activity in the period.
                    retention_rate:
                      type: number
                      description: >-
                        `active_persons` divided by `cohort_size`, between 0 and
                        1.
                    revenue:
                      type: number
                      description: >-
                        USD the cohort recorded in the period. Revenue
                        retention, not new revenue.
        granularity:
          type: string
          enum:
            - day
            - week
            - month
        date_range:
          type: object
          additionalProperties: true
          properties:
            start:
              type: string
              description: Resolved first cohort date, `YYYY-MM-DD`.
            end:
              type: string
              description: Resolved last cohort date, `YYYY-MM-DD`.
        complete:
          type: boolean
          description: >-
            Always `true` on a 200. A failed read is a `503`, never an empty
            cohort grid.
        meta:
          type: object
          additionalProperties: true
          properties:
            grain:
              type: string
              description: Always `person`.
            metric:
              type: string
              description: Always `activity` in this version.
            max_periods:
              type: integer
            cohort_count:
              type: integer
            period_unit:
              type: string
            cohort_basis:
              type: string
              description: >-
                `all_time_first_seen`. Cohorts are never scoped to the requested
                range.
            limitation:
              type: string
              description: States the activity-retention-only scope in prose.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Datalyr Agent key (dk_agent_…)

````