openapi: 3.1.0
info:
  title: HeyTaco API
  version: '1.0.0'
  description: The HeyTaco public REST API.
servers:
  - url: https://api.heytaco.com
security:
  - bearerAuth: []
tags:
  - name: leaderboard
    description: Leaderboard reports.
paths:
  /leaderboard:
    get:
      operationId: getLeaderboard
      summary: Get a leaderboard
      description: Returns the leaderboard for the caller's team.
      tags: [leaderboard]
      x-heytaco-scopes:
        - leaderboard
        - leaderboard:all
      parameters:
        - name: type
          in: query
          required: true
          schema: { type: string, enum: [received, given, combined] }
          description: The kind of totals to rank by.
        - name: timeframe
          in: query
          required: true
          schema: { type: string, enum: [custom, alltime, year, month, week, today] }
          example: month
          description: Leaderboard timeframe. Selecting `custom` also requires `start_date`/`end_date`. When `type=combined`, only `alltime` and `year` are supported.
        - name: channel_id
          in: query
          required: false
          schema: { type: string, minLength: 1 }
          description: Filter leaderboard to taco messages sent in a single channel. Not allowed when `type=combined`.
        - name: timezone
          in: query
          required: false
          schema: { type: string, default: America/Los_Angeles }
          example: America/New_York
          description: An IANA time zone name.
        - name: start_date
          in: query
          required: false
          schema: { type: string, format: date }
          description: Required and only valid when `timeframe=custom`. Format `YYYY-MM-DD`, interpreted in `timezone`.
        - name: end_date
          in: query
          required: false
          schema: { type: string, format: date }
          description: Required and only valid when `timeframe=custom`. Format `YYYY-MM-DD`, interpreted in `timezone`. Inclusive.
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1 }
          description: Maximum number of entries to return in `data`. When omitted, all entries are returned. `total_results` in the response reflects the total count before this limit is applied.
      responses:
        '200':
          description: A leaderboard report.
          headers:
            RateLimit: { $ref: '#/components/headers/RateLimit' }
            RateLimit-Policy: { $ref: '#/components/headers/RateLimit-Policy' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Leaderboard' }
        '400': { $ref: '#/components/responses/InvalidLeaderboardRequest' }
        '401': { $ref: '#/components/responses/NotAuthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimitExceeded' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: htk_...
  schemas:
    Leaderboard:
      type: object
      description: |
        A ranked list of users for a given team, timeframe (in the requested `timezone`), and taco-total type.
        Every leaderboard endpoint returns this shape.
      required: [object, type, timeframe, timezone, start_date, end_date, channel_id, total_results, limit, data]
      properties:
        object:
          type: string
          enum: [leaderboard]
          description: Object type. Always `leaderboard`.
        type:
          type: string
          enum: [received, given, combined]
          description: The kind of totals ranked. One of `received`, `given`, or `combined`.
        timeframe:
          type: string
          enum: [custom, alltime, year, month, week, today]
          description: The window used to compute the leaderboard.
        timezone:
          type: string
          example: America/New_York
          description: |
            The time zone the window was computed in, in canonical form (e.g. `US/Pacific` →
            `America/Los_Angeles`). `America/Los_Angeles` when omitted.
        start_date:
          type: [string, 'null']
          format: date
          description: '`null` for `alltime`; echoed for `custom`; computed in `timezone` for named timeframes.'
        end_date:
          type: [string, 'null']
          format: date
          description: '`null` for `alltime`; echoed for `custom`; computed in `timezone` for named timeframes.'
        channel_id:
          type: [string, 'null']
          description: Echo of the request `channel_id`, or `null` when omitted.
        total_results:
          type: integer
          description: Total number of entries available for the request, before `limit` is applied.
        limit:
          type: [integer, 'null']
          description: Echo of the request `limit`, or `null` when omitted.
        data:
          type: array
          description: Ranked entries in descending order by `total`.
          items: { $ref: '#/components/schemas/LeaderboardEntry' }
    LeaderboardEntry:
      type: object
      required: [rank, user_id, name, avatar_url, total]
      properties:
        rank: { type: integer, description: 1-indexed rank in the returned window. }
        user_id: { type: string, description: 'User id in: `U...` format' }
        name: { type: string, description: User's display name. }
        avatar_url:
          type: [string, 'null']
          format: uri
          description: URL of the user's avatar image, or `null` if unset.
        total: { type: integer, description: 'The taco total for the requested `type`.' }
        total_given: { type: integer, description: 'Total tacos given. Only present on `combined` rows.' }
        total_received: { type: integer, description: 'Total tacos received. Only present on `combined` rows.' }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable, machine-readable identifier. Safe to branch on.
            message:
              type: string
              description: Human-readable explanation. May change over time; do not branch on this value.
  responses:
    NotAuthenticated:
      description: Authentication failed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            not_authenticated: { $ref: '#/components/examples/not_authenticated' }
            invalid_token: { $ref: '#/components/examples/invalid_token' }
    Forbidden:
      description: |
        The request was rejected by an access-control check. This covers requests
        made over plain HTTP, scope, team-level API access being disabled, and
        tenant-state errors returned when the team or user is no longer eligible
        to call the API.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            https_required: { $ref: '#/components/examples/https_required' }
            insufficient_scope: { $ref: '#/components/examples/insufficient_scope' }
            leaderboard_type_not_enabled: { $ref: '#/components/examples/leaderboard_type_not_enabled' }
            api_disabled: { $ref: '#/components/examples/api_disabled' }
            team_inactive: { $ref: '#/components/examples/team_inactive' }
            user_inactive: { $ref: '#/components/examples/user_inactive' }
            invalid_team: { $ref: '#/components/examples/invalid_team' }
            user_not_in_team: { $ref: '#/components/examples/user_not_in_team' }
    InvalidLeaderboardRequest:
      description: Request validation error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            invalid_request: { $ref: '#/components/examples/invalid_request' }
            invalid_type: { $ref: '#/components/examples/invalid_type' }
            invalid_timeframe: { $ref: '#/components/examples/invalid_timeframe' }
            invalid_channel: { $ref: '#/components/examples/invalid_channel' }
            invalid_timezone: { $ref: '#/components/examples/invalid_timezone' }
            invalid_date_range: { $ref: '#/components/examples/invalid_date_range' }
            date_range_too_large: { $ref: '#/components/examples/date_range_too_large' }
            invalid_limit: { $ref: '#/components/examples/invalid_limit' }
    RateLimitExceeded:
      description: Rate limit exceeded — see `Retry-After`.
      headers:
        Retry-After: { $ref: '#/components/headers/Retry-After' }
        RateLimit: { $ref: '#/components/headers/RateLimit' }
        RateLimit-Policy: { $ref: '#/components/headers/RateLimit-Policy' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            rate_limited: { $ref: '#/components/examples/rate_limited' }
    InternalError:
      description: |
        An unexpected error on our side. Not caused by the request; safe to retry
        after a short delay, and reported to us automatically.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            internal_error: { $ref: '#/components/examples/internal_error' }
    ServiceUnavailable:
      description: |
        A dependent service is unavailable. Callers should retry with backoff.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            service_unavailable: { $ref: '#/components/examples/service_unavailable' }
  examples:
    not_authenticated:
      summary: not_authenticated
      description: Missing or malformed Authorization header.
      value: { error: { code: not_authenticated, message: 'Provide your API token as a Bearer token in the Authorization header.' } }
    invalid_token:
      summary: invalid_token
      description: Unknown, revoked, or expired token.
      value: { error: { code: invalid_token, message: 'The provided API token is invalid.' } }
    https_required:
      summary: https_required
      description: The request was made over plain HTTP. Use `https://`.
      value: { error: { code: https_required, message: 'This API only accepts HTTPS requests. Use https://api.heytaco.com.' } }
    insufficient_scope:
      summary: insufficient_scope
      description: Token lacks a required scope.
      value: { error: { code: insufficient_scope, message: 'This token lacks a required scope.' } }
    leaderboard_type_not_enabled:
      summary: leaderboard_type_not_enabled
      description: The requested leaderboard `type` is not enabled for this workspace. Returned to `leaderboard`-scoped tokens; `leaderboard:all` tokens bypass this check.
      value: { error: { code: leaderboard_type_not_enabled, message: 'This leaderboard type is not enabled for this workspace.' } }
    api_disabled:
      summary: api_disabled
      description: The team has disabled API access.
      value: { error: { code: api_disabled, message: 'Your team has disabled the API.' } }
    invalid_request:
      summary: invalid_request
      description: Unknown parameter, missing required field, or cross-field rule violation.
      value: { error: { code: invalid_request, message: 'Unknown parameter, missing required field, or cross-field rule violation.' } }
    invalid_type:
      summary: invalid_type
      description: '`type` is not one of `received` | `given` | `combined`.'
      value: { error: { code: invalid_type, message: 'type must be one of: received, given, combined.' } }
    invalid_timeframe:
      summary: invalid_timeframe
      description: '`timeframe` is not in the allowed set or violates the combined-only rule.'
      value: { error: { code: invalid_timeframe, message: 'timeframe must be one of: custom, alltime, year, month, week, today.' } }
    invalid_channel:
      summary: invalid_channel
      description: '`channel_id` was empty or invalid.'
      value: { error: { code: invalid_channel, message: 'channel_id must be a non-empty string.' } }
    invalid_timezone:
      summary: invalid_timezone
      description: '`timezone` is not a recognized time zone.'
      value: { error: { code: invalid_timezone, message: 'timezone must be an IANA time zone name, e.g. America/New_York.' } }
    invalid_date_range:
      summary: invalid_date_range
      description: '`start_date`/`end_date` failed format or ordering rules.'
      value: { error: { code: invalid_date_range, message: 'start_date must be on or before end_date.' } }
    date_range_too_large:
      summary: date_range_too_large
      description: Custom range exceeds the 2-year maximum.
      value: { error: { code: date_range_too_large, message: 'The custom date range exceeds the 2-year maximum.' } }
    invalid_limit:
      summary: invalid_limit
      description: '`limit` was not a positive integer.'
      value: { error: { code: invalid_limit, message: 'limit must be a positive integer.' } }
    rate_limited:
      summary: rate_limited
      description: Rate limit exceeded — see `Retry-After` and the `RateLimit-*` headers.
      value: { error: { code: rate_limited, message: 'Too many requests for this workspace. Another integration in this workspace may be consuming the quota.' } }
    team_inactive:
      summary: team_inactive
      description: The workspace is deactivated in HeyTaco (e.g. the subscription has lapsed).
      value: { error: { code: team_inactive, message: 'This workspace is no longer active.' } }
    user_inactive:
      summary: user_inactive
      description: The user who owns the token has been deactivated. Their tokens are revoked in the same operation, but a token in a mid-flight request may still surface this code.
      value: { error: { code: user_inactive, message: 'The user for this token is no longer active.' } }
    invalid_team:
      summary: invalid_team
      description: The workspace could not be found by the upstream service.
      value: { error: { code: invalid_team, message: 'This workspace could not be found.' } }
    user_not_in_team:
      summary: user_not_in_team
      description: The token's owner is no longer a member of the workspace.
      value: { error: { code: user_not_in_team, message: 'The user for this token is no longer a member of this workspace.' } }
    service_unavailable:
      summary: service_unavailable
      description: A dependent service is unreachable or returned a 5xx. Retry with backoff.
      value: { error: { code: service_unavailable, message: 'The service is temporarily unavailable. Please try again.' } }
    internal_error:
      summary: internal_error
      description: An unexpected server-side error. Not caused by the request.
      value: { error: { code: internal_error, message: 'Something went wrong.' } }
  headers:
    Retry-After:
      description: Seconds to wait before retrying.
      schema: { type: integer }
    RateLimit:
      description: Current rate-limit state for this workspace (RFC 9110-style, e.g. `"limit=100, remaining=42, reset=30"`). Authoritative — prefer this over any documented limits.
      schema: { type: string }
    RateLimit-Policy:
      description: Declared rate-limit policy (e.g. `"100;w=60"`). Authoritative — limits may be adjusted or added over time.
      schema: { type: string }
  x-heytaco-scopes:
    leaderboard:
      description: Access to leaderboard types your team has enabled. If an admin turns a leaderboard on or off, this scope follows.
      grant: member
    leaderboard:all:
      description: Access to all leaderboard types, regardless of your team's leaderboard settings.
      grant: approval
