openapi: 3.0.0
info:
  title: tny.dev API
  description: The tny.dev API allows you to programmatically create short links and retrieve analytics data. We offer authenticated API v1 endpoints with rate limiting, custom slugs, and detailed analytics.
  version: 1.0.0
  contact:
    name: tny.dev Support
    url: https://www.tny.dev
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
servers:
  - url: https://www.tny.dev
    description: Production server
security:
  - ApiKeyAuth: []
paths:
  /api/v1/shorten:
    post:
      summary: Create Short Link
      description: Creates a new shortened URL. Requires API key authentication. Custom slugs and domains require Developer tier.
      operationId: createShortLink
      tags:
        - Links
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  format: uri
                  description: The URL to shorten. Must be a valid HTTP/HTTPS URL. Cannot be localhost or private IP.
                  example: https://github.com/vercel/next.js
                customSlug:
                  type: string
                  pattern: ^[a-zA-Z0-9_-]{3,50}$
                  description: Custom slug for the short URL. Must be 3-50 characters, alphanumeric with hyphens and underscores. Requires Developer tier and custom domain.
                  example: my-custom-slug
                domain_id:
                  type: string
                  format: uuid
                  description: UUID of your custom domain. Creates short link on your domain instead of tny.dev.
                  example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '201':
          description: Short link created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  short_url:
                    type: string
                    format: uri
                    example: https://www.tny.dev/abc123
                  slug:
                    type: string
                    example: abc123
                  long_url:
                    type: string
                    format: uri
                    example: https://example.com
                  created_at:
                    type: string
                    format: date-time
                    example: '2024-01-07T10:00:00Z'
                  qr_download_url:
                    type: string
                    format: uri
                    example: https://www.tny.dev/api/qr/abc123
                  custom_domain:
                    type: string
                    description: Present when custom domain is used
                    example: yourdomain.com
                  domain_id:
                    type: string
                    description: Present when custom domain is used
                    format: uuid
                    example: 123e4567-e89b-12d3-a456-426614174000
        '400':
          description: Bad Request - Invalid URL, unsafe content, or invalid request body
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Error'
                  - $ref: '#/components/schemas/UnsafeUrlError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '409':
          description: Conflict - Custom slug already exists for this domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Unprocessable Entity - Invalid custom slug format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
  
  /api/v1/shorten/bulk:
    post:
      summary: Bulk Create Short Links
      description: Create multiple shortened URLs in a single request. Requires Developer tier subscription. Maximum 100 links per request.
      operationId: bulkCreateShortLinks
      tags:
        - Links
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - links
              properties:
                links:
                  type: array
                  maxItems: 100
                  description: Array of link objects to create (max 100)
                  items:
                    type: object
                    required:
                      - url
                    properties:
                      url:
                        type: string
                        format: uri
                        description: The URL to shorten
                        example: https://github.com/vercel/next.js
                      customSlug:
                        type: string
                        pattern: ^[a-zA-Z0-9_-]{3,50}$
                        description: Custom slug for this link
                        example: example
                      domain_id:
                        type: string
                        format: uuid
                        description: Custom domain UUID for this link
                        example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '201':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  tier:
                    type: string
                    example: developer
                  total:
                    type: integer
                    example: 3
                  created:
                    type: integer
                    example: 2
                  failed:
                    type: integer
                    example: 1
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                          example: 0
                        short_url:
                          type: string
                          format: uri
                          example: https://www.tny.dev/abc123
                        slug:
                          type: string
                          example: abc123
                        long_url:
                          type: string
                          format: uri
                          example: https://github.com/vercel/next.js
                        created_at:
                          type: string
                          format: date-time
                          example: '2024-01-07T10:00:00Z'
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                          example: 2
                        url:
                          type: string
                          format: uri
                          example: https://malicious-site.com
                        error:
                          type: string
                          example: URL has been flagged as unsafe
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
  
  /api/v1/links:
    get:
      summary: List Links
      description: Lists all links created with your API key. Supports pagination and includes click counts. Use the scope parameter to fetch all links across all your API keys.
      operationId: listLinks
      tags:
        - Links
      parameters:
        - name: page
          in: query
          description: Page number (default 1)
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: Items per page (default 20, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: scope
          in: query
          description: Scope of links to return
          schema:
            type: string
            enum: [key, all]
            default: key
      responses:
        '200':
          description: List of links
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Your hourly request limit
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Requests remaining this hour
            X-RateLimit-Reset:
              schema:
                type: integer
              description: Time when limit resets (Unix timestamp)
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          example: abc123
                        short_url:
                          type: string
                          format: uri
                          example: https://www.tny.dev/abc123
                        long_url:
                          type: string
                          format: uri
                          example: https://example.com
                        created_at:
                          type: string
                          format: date-time
                          example: '2024-01-07T12:00:00.000Z'
                        click_count:
                          type: integer
                          example: 42
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                        example: 1
                      limit:
                        type: integer
                        example: 20
                      total:
                        type: integer
                        example: 150
                      total_pages:
                        type: integer
                        example: 8
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
  
  /api/v1/analytics/{slug}:
    get:
      summary: Get Link Analytics
      description: Retrieves comprehensive analytics data for a shortened link. Requires API key authentication. You can access analytics for any link created by any of your API keys.
      operationId: getLinkAnalytics
      tags:
        - Analytics
      parameters:
        - name: slug
          in: path
          required: true
          description: The short link slug
          schema:
            type: string
            example: abc123
      responses:
        '200':
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    type: object
                    properties:
                      id:
                        type: string
                        example: abc123
                      long_url:
                        type: string
                        format: uri
                        example: https://example.com
                      created_at:
                        type: string
                        format: date-time
                        example: '2024-01-07T10:00:00Z'
                  analytics:
                    type: object
                    properties:
                      totalClicks:
                        type: integer
                        example: 1250
                      actualClicks:
                        type: integer
                        example: 1000
                      previews:
                        type: integer
                        example: 200
                      qrScans:
                        type: integer
                        example: 30
                      eventTypes:
                        type: object
                        properties:
                          click:
                            type: integer
                            example: 1000
                          preview:
                            type: integer
                            example: 200
                          qr_scan:
                            type: integer
                            example: 30
                      devices:
                        type: object
                        additionalProperties:
                          type: integer
                        example:
                          mobile: 600
                          desktop: 550
                          tablet: 100
                      countries:
                        type: object
                        additionalProperties:
                          type: integer
                        example:
                          US: 500
                          UK: 200
                          CA: 150
                      referrers:
                        type: object
                        additionalProperties:
                          type: integer
                        example:
                          twitter.com: 300
                          linkedin.com: 200
                      hourlyClicks:
                        type: object
                        description: 24 hour breakdown
                        additionalProperties:
                          type: integer
                      utm:
                        type: object
                        description: UTM parameter breakdown
                        additionalProperties:
                          type: object
                      recentClicks:
                        type: array
                        description: Last 10 clicks
                        items:
                          type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden - No access to this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Link not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
  
  /{slug}:
    get:
      summary: Redirect to Long URL
      description: Public endpoint that redirects to the original URL and tracks the click event. No authentication required.
      operationId: redirectShortLink
      tags:
        - Redirect
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          description: The short link slug
          schema:
            type: string
            example: abc123
      responses:
        '301':
          description: Redirects to the original URL
          headers:
            Location:
              schema:
                type: string
                format: uri
              description: The original URL
        '404':
          description: Short link not found
    head:
      summary: Check Link Existence
      description: Check if a short link exists without redirecting. No authentication required.
      operationId: checkShortLink
      tags:
        - Redirect
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          description: The short link slug
          schema:
            type: string
            example: abc123
      responses:
        '200':
          description: Link exists
        '404':
          description: Short link not found

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key starting with 'tnyl_'
  
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
          example: Invalid URL. Please provide a valid HTTP/HTTPS URL.
    
    UnsafeUrlError:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
          example: This URL has been flagged for containing malware
        code:
          type: string
          enum: [UNSAFE_URL]
          example: UNSAFE_URL
        threat_types:
          type: array
          items:
            type: string
            enum: [MALWARE, SOCIAL_ENGINEERING, UNWANTED_SOFTWARE, SOCIAL_ENGINEERING_EXTENDED_COVERAGE]
          example: [MALWARE]
  
  responses:
    Unauthorized:
      description: Unauthorized - Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    
    PaymentRequired:
      description: Payment Required - Feature requires Developer subscription
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            custom_slugs:
              value:
                error: Custom slugs require Developer plan
            custom_domains:
              value:
                error: Custom domains require Developer plan
            bulk_operations:
              value:
                error: Bulk operations require Developer plan
            webhooks:
              value:
                error: Webhooks require Developer plan
    
    TooManyRequests:
      description: Too Many Requests - Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Your hourly request limit
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining this hour
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Time when limit resets (Unix timestamp)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    
    InternalServerError:
      description: Internal Server Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

tags:
  - name: Links
    description: Operations related to creating and managing short links
  - name: Analytics
    description: Operations related to link analytics and statistics
  - name: Redirect
    description: Public redirect endpoint for short links