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

# Soundlink listening insights

> What listeners who connected through this soundlink went on to play.
The date window comes from the listening graph (`nodes_smart_link`).
`startDate` and `endDate` are not accepted. Extra query parameters are ignored.

This request often takes 30–50 seconds. Use a client timeout of at least 120 seconds.
The Send button on this page stops around 30 seconds. The API keeps running and can still return 200.
If the API Gateway deadline elapses, the response is `504`. That body is the gateway error, not the Public API envelope.

- `other-artists` — other artists these listeners streamed, excluding attributed tracks and campaign artists. Top 30.
- `catalog-dig-in` — playlist reach and catalog follow-through per artist. Track soundlinks may return `artists: []`.
- `stream-split` — `track` soundlinks return attributed contexts such as `promoted` and `artist_catalog`. `playlist` soundlinks return `playlist` then `artist_catalog`. A context is omitted when listeners and streams are both 0. Any other campaign type returns `200` with `contexts: []`.

Requires the `soundlinks:read` scope. Accepts API key or OAuth Bearer.
A soundlink created for a campaign is allowed here when the organization owns it,
even though `GET /v1/soundlinks/{soundlinkId}` does not return campaign soundlinks.
An unknown soundlink, or one owned by another organization, returns `404 not_found`.
An owned soundlink with no listening-graph row returns `404 not_in_listening_graph`
(`Soundlink not available in listening graph`).




## OpenAPI

````yaml /openapi/soundlink-public-api-v1.yaml get /v1/soundlinks/{soundlinkId}/listening-insights/{insight}
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/soundlinks/{soundlinkId}/listening-insights/{insight}:
    get:
      tags:
        - Soundlinks
      summary: Soundlink listening insights
      description: >
        What listeners who connected through this soundlink went on to play.

        The date window comes from the listening graph (`nodes_smart_link`).

        `startDate` and `endDate` are not accepted. Extra query parameters are
        ignored.


        This request often takes 30–50 seconds. Use a client timeout of at least
        120 seconds.

        The Send button on this page stops around 30 seconds. The API keeps
        running and can still return 200.

        If the API Gateway deadline elapses, the response is `504`. That body is
        the gateway error, not the Public API envelope.


        - `other-artists` — other artists these listeners streamed, excluding
        attributed tracks and campaign artists. Top 30.

        - `catalog-dig-in` — playlist reach and catalog follow-through per
        artist. Track soundlinks may return `artists: []`.

        - `stream-split` — `track` soundlinks return attributed contexts such as
        `promoted` and `artist_catalog`. `playlist` soundlinks return `playlist`
        then `artist_catalog`. A context is omitted when listeners and streams
        are both 0. Any other campaign type returns `200` with `contexts: []`.


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

        A soundlink created for a campaign is allowed here when the organization
        owns it,

        even though `GET /v1/soundlinks/{soundlinkId}` does not return campaign
        soundlinks.

        An unknown soundlink, or one owned by another organization, returns `404
        not_found`.

        An owned soundlink with no listening-graph row returns `404
        not_in_listening_graph`

        (`Soundlink not available in listening graph`).
      operationId: getSoundlinkListeningInsight
      parameters:
        - $ref: '#/components/parameters/soundlinkId'
        - $ref: '#/components/parameters/listeningInsight'
      responses:
        '200':
          description: >-
            Listening insight for the soundlink window. The `data.insight` value
            matches the path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListeningInsightsResponse'
        '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'
        '504':
          description: >
            The API Gateway deadline of 120 seconds elapsed. The body is the
            gateway error, not the Public API envelope (`data` or `error`, plus
            `meta`).
      security:
        - apiKeyBearerAuth: []
        - oauthBearer: []
components:
  parameters:
    soundlinkId:
      name: soundlinkId
      in: path
      required: true
      schema:
        type: string
      description: >-
        Soundlink identifier (URL-safe string, same ID used in the public
        sndl.ink URL)
    listeningInsight:
      name: insight
      in: path
      required: true
      schema:
        type: string
        enum:
          - other-artists
          - catalog-dig-in
          - stream-split
      description: |
        `other-artists` — what else these listeners stream.
        `catalog-dig-in` — playlist artists and catalog follow-through.
        `stream-split` — promoted, playlist, and artist-catalog split.
  schemas:
    ListeningInsightsResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          oneOf:
            - $ref: '#/components/schemas/OtherArtistsListeningInsight'
            - $ref: '#/components/schemas/CatalogDigInListeningInsight'
            - $ref: '#/components/schemas/StreamSplitListeningInsight'
          discriminator:
            propertyName: insight
            mapping:
              other-artists: '#/components/schemas/OtherArtistsListeningInsight'
              catalog-dig-in: '#/components/schemas/CatalogDigInListeningInsight'
              stream-split: '#/components/schemas/StreamSplitListeningInsight'
        meta:
          $ref: '#/components/schemas/Meta'
    OtherArtistsListeningInsight:
      type: object
      required:
        - soundlinkId
        - campaignId
        - insight
        - window
        - artists
      properties:
        soundlinkId:
          type: string
        campaignId:
          type: string
          nullable: true
        insight:
          type: string
          enum:
            - other-artists
        window:
          $ref: '#/components/schemas/ListeningInsightWindow'
        artists:
          type: array
          maxItems: 30
          items:
            type: object
            required:
              - artist
              - listeners
              - streams
            properties:
              artist:
                type: string
                description: Lowercased, trimmed artist name.
              listeners:
                type: number
              streams:
                type: number
    CatalogDigInListeningInsight:
      type: object
      required:
        - soundlinkId
        - campaignId
        - insight
        - window
        - artists
      properties:
        soundlinkId:
          type: string
        campaignId:
          type: string
          nullable: true
        insight:
          type: string
          enum:
            - catalog-dig-in
        window:
          $ref: '#/components/schemas/ListeningInsightWindow'
        artists:
          type: array
          maxItems: 30
          items:
            type: object
            required:
              - artist
              - playlistListeners
              - playlistStreams
              - catalogListeners
              - catalogStreams
              - catalogTracks
              - catalogListenerRate
            properties:
              artist:
                type: string
              playlistListeners:
                type: number
              playlistStreams:
                type: number
              catalogListeners:
                type: number
              catalogStreams:
                type: number
              catalogTracks:
                type: number
              catalogListenerRate:
                type: number
                nullable: true
                description: >-
                  catalogListeners / playlistListeners. Null when
                  playlistListeners is 0.
    StreamSplitListeningInsight:
      type: object
      required:
        - soundlinkId
        - campaignId
        - insight
        - window
        - contexts
      properties:
        soundlinkId:
          type: string
        campaignId:
          type: string
          nullable: true
        insight:
          type: string
          enum:
            - stream-split
        window:
          $ref: '#/components/schemas/ListeningInsightWindow'
        contexts:
          type: array
          description: >-
            Empty when the graph `campaign_type` is neither `track` nor
            `playlist`. The response is still `200`.
          items:
            $ref: '#/components/schemas/StreamSplitContext'
    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'
    ListeningInsightWindow:
      type: object
      required:
        - startDate
        - endDate
      properties:
        startDate:
          type: string
          format: date
          example: '2026-07-30'
        endDate:
          type: string
          format: date
          example: '2026-09-29'
    StreamSplitContext:
      type: object
      required:
        - engagementContext
        - listeners
        - streams
        - tracks
      properties:
        engagementContext:
          type: string
          description: '`promoted`, `playlist`, or `artist_catalog`.'
        listeners:
          type: number
        streams:
          type: number
        tracks:
          type: number
    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.