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

# Campaign cohort retention

> Cohort retention for a campaign's linked soundlink: one row per cohort date
that has first plays (dates with no first plays have no row), where the
cohort date is the UTC date listeners first played via the campaign soundlink.
A listener's cohort date is their earliest first play across all countries.
Conforms to the `soundlink_cohort_retention_daily` v1.0 schema (same row
shape as `GET /v1/soundlinks/{soundlinkId}/metrics/retention`). All rows
are returned in `data.items` (one per cohort date), so there is no
pagination.

**Retention definitions:**
- `returned_on_day_N`: listeners from the cohort who played again on exactly day N
  after their first play.
- `retention_by_day_N`: share of the cohort (`cohort_size`) who played again on any
  day from 1 to N (end-day, cumulative). Each listener is counted once, so the
  by-day rate never decreases within a row.
- A day-N value is `null` until day N has passed for that cohort date.

**Known limitation:** end-day rates are cumulative per listener, so whether
day-3 returners also returned on day 2 is not recoverable from the end-day
columns. Use the exact-day `returned_on_day_N` columns for day-by-day behaviour.

`soundlink_target_isrc` is currently always `null`.

**Date range:** `startDate` / `endDate` filter the cohort date (both inclusive).
If you omit both, a recent window ending today is used. A range that is too wide
returns `400 invalid_query_parameter`.

Requires the `metrics:read` scope. Accepts API key or OAuth Bearer.
A campaign outside the organization returns `404 campaign_not_found`.
An owned campaign with no linked soundlink returns `404 not_found`
(`Campaign soundlink not found.`).




## OpenAPI

````yaml /openapi/soundlink-public-api-v1.yaml get /v1/campaigns/{campaignId}/metrics/retention
openapi: 3.0.3
info:
  title: Soundlink Public API
  version: 0.0.1-draft
  description: >
    Campaign listing, metrics, and write operations for Soundlink partner
    organizations.


    **Base URL:** `https://api.getsoundlink.com`


    ## Authentication


    Send `Authorization: Bearer` with one of:


    1. **API key (recommended)** — a `sk_*` key from the Soundlink app
       (Settings → Developer → API keys):
       ```
       Authorization: Bearer sk_abc123_<token>
       ```

    2. **OAuth Bearer** — an access token from Sign in with Soundlink
       (authorization code or client credentials). Token scopes use the
       same strings as Public API key scopes (for example `campaigns:read`,
       `metrics:read`):
       ```
       Authorization: Bearer <oauth_access_token>
       ```

    `x-api-key` is no longer accepted and is ignored if sent. Requests

    without `Authorization: Bearer` return `401 invalid_api_key` (or

    `401 invalid_token` when a Bearer value is present but neither a

    recognized `sk_*` key nor a valid OAuth access token).


    ### Which operations accept OAuth?


    | Operations | Required scope | Auth |

    | --- | --- | --- |

    | `GET /v1/ping` | any granted Public API scope | API key (Bearer) or OAuth
    Bearer |

    | Campaign reads (`GET /v1/strategies`, `GET /v1/campaigns`, campaign
    detail, tiers) | `campaigns:read` | API key (Bearer) or OAuth Bearer |

    | Metrics reads (overview, breakdown, exports, campaign listening insights,
    campaign retention) | `metrics:read` | API key (Bearer) or OAuth Bearer |

    | Campaign writes (create, stop, budget, tier updates) | `campaigns:write` |
    API key (Bearer) or OAuth Bearer |

    | Video import / import status | `videos:write` | API key (Bearer) or OAuth
    Bearer |

    | Soundlink reads (list, detail, metrics
    overview/breakdown/engagement/retention, listening insights, JSONL exports)
    | `soundlinks:read` | API key (Bearer) or OAuth Bearer |

    | Soundlink writes (create, archive, test events) | `soundlinks:write` | API
    key (Bearer) or OAuth Bearer |

    | Wallet read (`GET /v1/wallet`) | `wallet:read` | API key (Bearer) or OAuth
    Bearer |


    Per-operation scope requirements are also marked with `x-required-scope`

    (except `GET /v1/ping`, which accepts any valid credential with at least

    one Public API scope granted to the key/token).


    All responses are wrapped in a standard envelope:

    ```json

    { "data": <payload>, "meta": { "requestId": "<uuid>" } }

    ```


    Errors follow the same envelope shape with an `error` field instead of
    `data`.
servers:
  - url: https://api.getsoundlink.com
    description: Production
security:
  - apiKeyBearerAuth: []
  - oauthBearer: []
tags:
  - name: System
    description: Connectivity and auth verification
  - name: Wallet
    description: Organization wallet balance
  - name: Campaigns
    description: >-
      Campaign listing, detail, creation, and wallet management (budget, stop,
      tiers)
  - name: Metrics
    description: Campaign performance metrics
  - name: Videos
    description: Partner video library import for Full Control campaigns
  - name: Soundlinks
    description: Soundlink listing, detail, creation, archive, and performance metrics
paths:
  /v1/campaigns/{campaignId}/metrics/retention:
    get:
      tags:
        - Metrics
      summary: Campaign cohort retention
      description: >
        Cohort retention for a campaign's linked soundlink: one row per cohort
        date

        that has first plays (dates with no first plays have no row), where the

        cohort date is the UTC date listeners first played via the campaign
        soundlink.

        A listener's cohort date is their earliest first play across all
        countries.

        Conforms to the `soundlink_cohort_retention_daily` v1.0 schema (same row

        shape as `GET /v1/soundlinks/{soundlinkId}/metrics/retention`). All rows

        are returned in `data.items` (one per cohort date), so there is no

        pagination.


        **Retention definitions:**

        - `returned_on_day_N`: listeners from the cohort who played again on
        exactly day N
          after their first play.
        - `retention_by_day_N`: share of the cohort (`cohort_size`) who played
        again on any
          day from 1 to N (end-day, cumulative). Each listener is counted once, so the
          by-day rate never decreases within a row.
        - A day-N value is `null` until day N has passed for that cohort date.


        **Known limitation:** end-day rates are cumulative per listener, so
        whether

        day-3 returners also returned on day 2 is not recoverable from the
        end-day

        columns. Use the exact-day `returned_on_day_N` columns for day-by-day
        behaviour.


        `soundlink_target_isrc` is currently always `null`.


        **Date range:** `startDate` / `endDate` filter the cohort date (both
        inclusive).

        If you omit both, a recent window ending today is used. A range that is
        too wide

        returns `400 invalid_query_parameter`.


        Requires the `metrics:read` scope. Accepts API key or OAuth Bearer.

        A campaign outside the organization returns `404 campaign_not_found`.

        An owned campaign with no linked soundlink returns `404 not_found`

        (`Campaign soundlink not found.`).
      operationId: getCampaignRetention
      parameters:
        - $ref: '#/components/parameters/campaignId'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
      responses:
        '200':
          description: >-
            Cohort retention rows (`soundlink_cohort_retention_daily` schema),
            sorted by `cohort_date` ascending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SoundlinkCohortRetentionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - apiKeyBearerAuth: []
        - oauthBearer: []
components:
  parameters:
    campaignId:
      name: campaignId
      in: path
      required: true
      schema:
        type: string
      description: Campaign identifier (UUID)
    startDate:
      name: startDate
      in: query
      schema:
        type: string
        format: date
      description: |
        Inclusive start date (`YYYY-MM-DD`). When omitted, the default depends
        on the endpoint: campaign overview and paginated breakdown use the
        campaign start date; engagement and JSONL export endpoints use the
        last 90 days ending today (see each operation).
    endDate:
      name: endDate
      in: query
      schema:
        type: string
        format: date
      description: Inclusive end date (`YYYY-MM-DD`). Defaults to today.
  schemas:
    SoundlinkCohortRetentionResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          type: object
          required:
            - schemaVersion
            - items
          properties:
            schemaVersion:
              type: string
              description: >-
                Version of the `soundlink_cohort_retention_daily` row schema.
                Pin your ingest pipeline to this.
              example: '1.0'
            items:
              type: array
              items:
                $ref: '#/components/schemas/SoundlinkCohortRetentionDailyRow'
        meta:
          $ref: '#/components/schemas/Meta'
    SoundlinkCohortRetentionDailyRow:
      type: object
      description: |
        One row of the `soundlink_cohort_retention_daily` v1.0 schema.
        Primary key: `(provider, account_id, cohort_date, soundlink_id)`.
        Rows are idempotent. Re-ingesting on the primary key is safe.
        A day-N value is final once day N has passed for the cohort date.
        Until then it is `null`.
      required:
        - provider
        - account_id
        - schema_version
        - cohort_date
        - cohort_date_timezone
        - exported_at
        - soundlink_id
        - soundlink_name
        - soundlink_target_type
        - cohort_size
        - returned_on_day_1
        - retention_by_day_1
        - returned_on_day_2
        - retention_by_day_2
        - returned_on_day_3
        - retention_by_day_3
        - returned_on_day_4
        - retention_by_day_4
        - returned_on_day_5
        - retention_by_day_5
        - returned_on_day_6
        - retention_by_day_6
        - returned_on_day_7
        - retention_by_day_7
      properties:
        provider:
          type: string
          enum:
            - soundlink
          description: >-
            Always `soundlink`. Identifies the data provider for multi-source
            warehouses.
        account_id:
          type: string
          description: Soundlink organization ID (UUID).
        schema_version:
          type: string
          description: Schema version string. Pin your ingest pipeline to this value.
          example: '1.0'
        cohort_date:
          type: string
          format: date
          description: >-
            UTC calendar date (`YYYY-MM-DD`) on which this cohort's listeners
            first played via the soundlink.
        cohort_date_timezone:
          type: string
          enum:
            - UTC
          description: Always `UTC`.
        exported_at:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp when this row was generated.
        soundlink_id:
          type: string
          description: Unique ID of this soundlink.
          example: V1StGXR8_Z5jdHi6B-myT
        soundlink_name:
          type: string
          description: Human-readable soundlink name.
        soundlink_target_type:
          type: string
          nullable: true
          enum:
            - track
            - playlist
          description: >-
            Whether this soundlink promotes a track or playlist. `null` if
            undetermined.
        soundlink_target_isrc:
          type: string
          nullable: true
          description: ISRC of the promoted track. Currently always `null`.
        soundlink_target_spotify_track_id:
          type: string
          nullable: true
          description: >-
            Spotify track ID of the promoted track. Populated when
            `soundlink_target_type = track`; otherwise `null`.
        soundlink_target_playlist_id:
          type: string
          nullable: true
          description: >-
            Spotify playlist ID. Populated when `soundlink_target_type =
            playlist`; otherwise `null`.
        cohort_size:
          type: integer
          minimum: 0
          description: >-
            Unique listeners whose first play via this soundlink was on
            `cohort_date`.
        returned_on_day_1:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 1 after their first
            play. `null` until day 1 has passed.
        retention_by_day_1:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 1
            (end-day, cumulative), rounded to 4 decimals. `null` until day 1 has
            passed.
        returned_on_day_2:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 2 after their first
            play. `null` until day 2 has passed.
        retention_by_day_2:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 2
            (end-day, cumulative), rounded to 4 decimals. `null` until day 2 has
            passed.
        returned_on_day_3:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 3 after their first
            play. `null` until day 3 has passed.
        retention_by_day_3:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 3
            (end-day, cumulative), rounded to 4 decimals. `null` until day 3 has
            passed.
        returned_on_day_4:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 4 after their first
            play. `null` until day 4 has passed.
        retention_by_day_4:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 4
            (end-day, cumulative), rounded to 4 decimals. `null` until day 4 has
            passed.
        returned_on_day_5:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 5 after their first
            play. `null` until day 5 has passed.
        retention_by_day_5:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 5
            (end-day, cumulative), rounded to 4 decimals. `null` until day 5 has
            passed.
        returned_on_day_6:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 6 after their first
            play. `null` until day 6 has passed.
        retention_by_day_6:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 6
            (end-day, cumulative), rounded to 4 decimals. `null` until day 6 has
            passed.
        returned_on_day_7:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Cohort listeners who played again on exactly day 7 after their first
            play. `null` until day 7 has passed.
        retention_by_day_7:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Share of `cohort_size` who played again on any day from 1 to 7
            (end-day, cumulative), rounded to 4 decimals. `null` until day 7 has
            passed.
    Meta:
      type: object
      required:
        - requestId
      properties:
        requestId:
          type: string
          format: uuid
          description: Server-generated request ID. Include when contacting support.
    ErrorResponse:
      type: object
      required:
        - error
        - meta
      properties:
        error:
          $ref: '#/components/schemas/Error'
        meta:
          $ref: '#/components/schemas/Meta'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Stable snake_case error code.
          enum:
            - invalid_api_key
            - api_key_revoked
            - api_key_expired
            - insufficient_scope
            - not_found
            - not_in_listening_graph
            - campaign_not_found
            - invalid_query_parameter
            - invalid_date_range
            - page_size_exceeded
            - invalid_request
            - invalid_genre
            - invalid_duration_or_budget
            - invalid_daily_budget
            - invalid_duration_days
            - invalid_spotify_url
            - invalid_tier_budget_allocation
            - invalid_tier_status
            - tier_update_cooldown
            - wallet_not_enabled
            - insufficient_credit
            - idempotency_key_conflict
            - rate_limit_exceeded
            - invalid_video_url
            - video_import_limit_exceeded
            - video_import_session_not_found
            - internal_error
        message:
          type: string
          description: Human-readable error description. Do not parse programmatically.
        details:
          type: object
          nullable: true
          description: |
            Additive, error-code-specific context (e.g. `available`/`required`
            on `insufficient_credit`). Absent or `null` for error codes that
            carry no extra data.
          additionalProperties: true
  responses:
    BadRequest:
      description: Invalid query parameter or date range
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalid_query_parameter:
              value:
                error:
                  code: invalid_query_parameter
                  message: pageSize must be an integer between 1 and 500.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
            invalid_date_range:
              value:
                error:
                  code: invalid_date_range
                  message: endDate must not be before startDate.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
    Unauthorized:
      description: |
        Missing or invalid credentials. Covers missing/malformed API keys,
        invalid or expired OAuth access tokens, and revoked API keys.
        `x-api-key` is not accepted and is ignored if sent.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Present only on `rate_limit_exceeded`
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalid_api_key:
              value:
                error:
                  code: invalid_api_key
                  message: 'Missing credentials. Send Authorization: Bearer.'
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
            invalid_token:
              value:
                error:
                  code: invalid_token
                  message: OAuth access token is invalid or expired.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
            api_key_revoked:
              value:
                error:
                  code: api_key_revoked
                  message: >-
                    This API key has been revoked. Create a new key in Settings
                    → Developer → API keys.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: |
        Authenticated but not allowed: missing required scope, or OAuth access
        disabled for the organization (`access_denied`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            insufficient_scope:
              value:
                error:
                  code: insufficient_scope
                  message: This token does not have the metrics:read scope.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
            access_denied:
              value:
                error:
                  code: access_denied
                  message: OAuth access is not enabled for this organization.
                meta:
                  requestId: 550e8400-e29b-41d4-a716-446655440000
    NotFound:
      description: Campaign not found or belongs to another organization
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: campaign_not_found
              message: Campaign not found.
            meta:
              requestId: 550e8400-e29b-41d4-a716-446655440000
    RateLimited:
      description: Per-key rate limit exceeded (60/min, 600/hr)
      headers:
        Retry-After:
          required: true
          schema:
            type: integer
          description: Seconds until the rate limit resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limit_exceeded
              message: >-
                Rate limit exceeded. Retry after the number of seconds indicated
                in the Retry-After header.
            meta:
              requestId: 550e8400-e29b-41d4-a716-446655440000
    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: internal_error
              message: >-
                An unexpected error occurred. Contact support with the
                requestId.
            meta:
              requestId: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    apiKeyBearerAuth:
      type: http
      scheme: bearer
      description: |
        Pass your Soundlink `sk_*` API key as a Bearer token:
        `Authorization: Bearer sk_<prefix>_<token>` (not an OAuth token; see
        `oauthBearer` for that). Accepted on all Public API operations. Send
        only one `Authorization` header per request.
    oauthBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        OAuth 2.0 access token from Sign in with Soundlink
        (`Authorization: Bearer <oauth_access_token>`).
        Token scopes must include the operation's required scope
        (see `x-required-scope` and the Authentication section).
        Accepted on all Public API operations (reads and writes).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.