> ## Documentation Index
> Fetch the complete documentation index at: https://docs.teasy.link/api-v1/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Summary

> The only scope carrying clicks_by_widget: widget ids from different landing pages in one list would have nothing to attach to.



## OpenAPI

````yaml GET /v1/landings/{id}/analytics
openapi: 3.0.0
info:
  title: Teasy API
  description: >-
    Programmatic access to links, landing pages, groups, domains and analytics.


    ## Authentication


    Every request carries an API key in the `Authorization` header:


    ```

    Authorization: Bearer tsl_live_7Kx9mNpQ2rT3vB8dF4hJ6sW1yZ5aC0eG

    ```


    Keys are issued from the dashboard and shown once, at creation. **The key is
    never accepted

    as a query parameter** — query strings end up in proxy logs, browser history
    and `Referer`

    headers, which would leak it.


    The key is a server-side secret. Browser calls are not supported: a key
    shipped in a frontend

    bundle can be read by any visitor.


    ## Rate limits


    Two windows apply per account, both defined by your plan:


    | Header | Meaning |

    |---|---|

    | `RateLimit-Limit-Minute` | requests allowed per minute |

    | `RateLimit-Remaining-Minute` | left in the current minute |

    | `RateLimit-Limit-Day` | requests allowed per day |

    | `RateLimit-Remaining-Day` | left today |


    On `429` the response also carries `Retry-After` — seconds until the window
    that fired

    resets.


    **These headers are absent on `401` and on `429` returned before
    authentication**: with no

    identified account there is no plan and no quota to report. Read them
    defensively — code that

    parses them unconditionally will get `undefined` where it expects a number.


    All API keys of an account share the same budget, so one runaway integration
    affects the rest.


    ## Errors


    Every error, on every status, has the same shape:


    ```json

    {
      "error": {
        "code": "redirect_not_found",
        "message": "Redirect not found.",
        "request_id": "req_9f2c5a4b7d4c31a2f86e0d91b3c7aa10"
      }
    }

    ```


    **The contract is `code`, not `message`.** Messages are written for humans
    and may be

    reworded at any time; codes are stable. Validation failures add a `details`
    array with the

    offending field and a machine code per violation.


    Include `request_id` when contacting support — it is the only way to find
    your exact request

    in our logs.


    | Code | Meaning |

    |---|---|

    | `invalid_request` | Request validation failed. |

    | `invalid_api_key` | API key is missing, invalid or revoked. |

    | `account_suspended` | This account is suspended. |

    | `api_access_not_available_on_current_plan` | The API is not available on
    your current plan. |

    | `feature_not_available_on_current_plan` | This feature is not available on
    your current plan. |

    | `plan_limit_reached` | You have reached the limit of your current plan. |

    | `analytics_range_exceeds_plan_limit` | The requested period is longer than
    your plan allows. |

    | `domain_not_found` | Domain not found. |

    | `domain_not_active` | Domain exists but its verification is not complete
    yet. |

    | `slug_required_for_system_domain` | A slug is required on the system
    domain. |

    | `redirect_not_found` | Redirect not found. |

    | `landing_not_found` | Landing not found. |

    | `group_not_found` | Group not found. |

    | `slug_already_taken` | This address is already taken. |

    | `hostname_already_taken` | This hostname is already connected to another
    account. |

    | `hostname_is_system_domain` | This hostname belongs to the platform and
    cannot be added. |

    | `domain_provider_unavailable` | The domain provider is temporarily
    unavailable. Please retry. |

    | `analytics_not_configured` | This link has no analytics attached, so no
    statistics will ever be available for it. |

    | `payload_too_large` | Request body is too large. |

    | `unsupported_media_type` | Content-Type must be application/json. |

    | `rate_limit_exceeded` | Rate limit exceeded. |

    | `daily_quota_exceeded` | Daily quota exceeded. |

    | `analytics_temporarily_unavailable` | Analytics is temporarily
    unavailable. Please retry. |

    | `internal_error` | Internal server error. |

    | `duplicate_id_in_batch` | This id appears more than once in the batch. |

    | `duplicate_slug_in_batch` | Two items of the batch claim the same address.
    |


    ## Enumerated values grow


    Fields such as widget `type`, traffic `channel` or domain `status` are open
    sets: new values

    appear as the product grows, and that is **not** treated as a breaking
    change. Handle unknown

    values gracefully — an exhaustive `switch` without a default branch will
    break on the day a

    new icon ships.


    ## Pagination


    Lists are cursor-paginated. Pass `pagination.next_cursor` from the previous
    response back as

    `?cursor=`; when `has_more` is `false` there is nothing left to fetch.


    **The cursor is opaque.** It is not a page number, an id or a timestamp — do
    not parse,

    construct or modify it. Its internal format may change without notice, and
    only values we

    returned are guaranteed to work.


    There is no total count: computing it on every request would turn into a
    performance promise

    we would rather spend on serving your data.


    ## Time


    All timestamps in responses are ISO 8601 in UTC (`2026-08-21T10:00:00Z`).


    Analytics endpoints are the one exception on input: period bounds arrive as
    UTC milliseconds

    together with an IANA timezone, which is used to cut buckets in your users'
    local time.


    Two properties of analytics periods worth knowing:


    - both bounds are **inclusive**, so a range ending exactly where the next
    one starts counts an
      event on that edge twice;
    - an `end_date` in the future is silently clamped to the current server
    time. Two identical
      requests can therefore return different numbers — only a period that is entirely in the past
      is stable enough to compare across calls.
  version: '1'
  contact: {}
servers:
  - url: https://api.teasy.link
    description: Production
security: []
tags: []
paths:
  /v1/landings/{id}/analytics:
    get:
      tags:
        - Analytics
      summary: Landing page analytics summary
      description: >-
        The only scope carrying clicks_by_widget: widget ids from different
        landing pages in one list would have nothing to attach to.
      operationId: ExternalLandingAnalyticsController_summary
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
            example: lnd_5aec1b2c3d4e5f6a7b8c9d0e1f2a3b4c
        - name: start_date
          required: true
          in: query
          description: Start of the period, UTC milliseconds. Inclusive.
          schema:
            example: 1755734400000
            type: integer
        - name: end_date
          required: true
          in: query
          description: >-
            End of the period, UTC milliseconds. Inclusive, and silently clamped
            to the current server time if it lies in the future.
          schema:
            example: 1756339200000
            type: integer
        - name: timezone
          required: true
          in: query
          description: >-
            IANA timezone. Buckets and the activity grid are cut in it; totals
            do not depend on it.
          schema:
            maxLength: 64
            example: Europe/Belgrade
            type: string
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalLandingAnalytics'
        '401':
          description: API key is missing, malformed, revoked or unknown.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
        '403':
          description: Your plan does not include the API or this particular feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
        '404':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
        '409':
          description: >-
            The link has no analytics attached, so statistics will never be
            available for it. Unlike 503, retrying does not help.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
        '429':
          description: >-
            Rate limit or daily quota exceeded. Retry-After tells you when to
            come back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
        '500':
          description: Unexpected failure on our side.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalErrorResponse'
      security:
        - apiKey: []
components:
  schemas:
    ExternalLandingAnalytics:
      type: object
      properties:
        object:
          type: string
          enum:
            - analytics
        visitors:
          type: integer
          example: 1240
          description: >-
            Unique visitors, summed across the links of the scope. A person who
            visited two links counts twice.
        visits:
          type: integer
          example: 1580
        countries:
          nullable: true
          description: >-
            Top countries as ISO 3166-1 alpha-2 codes. Null when your plan does
            not include breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        referrers:
          nullable: true
          description: >-
            Top referring domains. Null when your plan does not include
            breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        channels:
          nullable: true
          description: >-
            Traffic channels, e.g. direct, organicSearch, paidAds. Null when
            your plan does not include breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        browsers:
          nullable: true
          description: Top browsers. Null when your plan does not include breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        operating_systems:
          nullable: true
          description: >-
            Top operating systems. Null when your plan does not include
            breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        devices:
          nullable: true
          description: >-
            Top device classes: desktop, mobile, tablet. Null when your plan
            does not include breakdowns.
          type: array
          items:
            $ref: '#/components/schemas/ExternalAnalyticsMetricItem'
        activity:
          nullable: true
          description: >-
            When the traffic arrives. Null when your plan does not include
            breakdowns.
          type: object
          allOf:
            - $ref: '#/components/schemas/ExternalAnalyticsActivity'
        clicks:
          type: integer
          example: 310
          description: Clicks on widgets of the landing pages in this scope.
        direct_button_clicks:
          type: integer
          example: 64
        tease_chat:
          $ref: '#/components/schemas/ExternalTeaseChatFunnel'
        clicks_by_widget:
          description: >-
            Rows are unique by widget_id + type + url + side, and their clicks
            sum up to the clicks field.
          type: array
          items:
            $ref: '#/components/schemas/ExternalWidgetClicks'
      required:
        - object
        - visitors
        - visits
        - countries
        - referrers
        - channels
        - browsers
        - operating_systems
        - devices
        - activity
        - clicks
        - direct_button_clicks
        - tease_chat
        - clicks_by_widget
    ExternalErrorResponse:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ExternalError'
      required:
        - error
    ExternalAnalyticsMetricItem:
      type: object
      properties:
        name:
          type: string
          example: US
          description: >-
            Raw key as recorded: an ISO country code, a referrer domain, a
            browser name. Human-readable names are up to you.
        visitors:
          type: integer
          example: 420
      required:
        - name
        - visitors
    ExternalAnalyticsActivity:
      type: object
      properties:
        by_hour:
          minItems: 24
          maxItems: 24
          description: >-
            Visits per hour of day, index 0 is midnight. Hours are cut in the
            timezone you passed.
          type: array
          items:
            type: number
        by_weekday:
          minItems: 7
          maxItems: 7
          description: Visits per weekday, index 0 is Sunday.
          type: array
          items:
            type: number
      required:
        - by_hour
        - by_weekday
    ExternalTeaseChatFunnel:
      type: object
      properties:
        message_1:
          type: integer
          example: 840
        message_2:
          type: integer
          example: 390
        redirects:
          type: integer
          example: 95
          description: Clicks on the chat button that led the visitor away.
      required:
        - message_1
        - message_2
        - redirects
    ExternalWidgetClicks:
      type: object
      properties:
        widget_id:
          type: string
          nullable: true
          example: wdg_b2690f1c4d5e6a7b8c9d0e1f2a3b4c5d
          description: >-
            Null means the clicks could not be attributed to a widget, not that
            the widget was deleted. May repeat across rows if the widget link
            changed.
        type:
          type: string
          nullable: true
          example: card
        url:
          type: string
          nullable: true
          example: https://onlyfans.com/kate
        side:
          type: string
          nullable: true
          example: left
          description: Filled for dual widgets only.
        clicks:
          type: integer
          example: 180
      required:
        - widget_id
        - type
        - url
        - side
        - clicks
    ExternalError:
      type: object
      properties:
        code:
          type: string
          enum:
            - invalid_request
            - invalid_api_key
            - account_suspended
            - api_access_not_available_on_current_plan
            - feature_not_available_on_current_plan
            - plan_limit_reached
            - analytics_range_exceeds_plan_limit
            - domain_not_found
            - domain_not_active
            - slug_required_for_system_domain
            - redirect_not_found
            - landing_not_found
            - group_not_found
            - slug_already_taken
            - hostname_already_taken
            - hostname_is_system_domain
            - domain_provider_unavailable
            - analytics_not_configured
            - payload_too_large
            - unsupported_media_type
            - rate_limit_exceeded
            - daily_quota_exceeded
            - analytics_temporarily_unavailable
            - internal_error
            - duplicate_id_in_batch
            - duplicate_slug_in_batch
          description: The contract is this code, not the message.
        message:
          type: string
          example: Redirect not found.
          description: Human-readable and subject to change. Do not match on it.
        request_id:
          type: string
          example: req_9f2c5a4b7d4c31a2f86e0d91b3c7aa10
          description: Quote this when contacting support.
        details:
          description: Present on validation failures, one entry per violation.
          type: array
          items:
            $ref: '#/components/schemas/ExternalErrorDetail'
      required:
        - code
        - message
        - request_id
    ExternalErrorDetail:
      type: object
      properties:
        field:
          type: string
          example: target_url
        code:
          type: string
          example: invalid_value
        message:
          type: string
          example: target_url must be a valid URL
      required:
        - field
        - code
        - message
  securitySchemes:
    apiKey:
      scheme: bearer
      bearerFormat: tsl_live_<32 characters>
      type: http
      description: >-
        API key issued in the dashboard. Server-side only: never ship it to a
        browser, and never pass it as a query parameter.

````