openapi: 3.1.0
info:
  title: SDN.ac API
  version: 1.0.0
  description: Public JSON API for creating, reading, and checking SDN.ac short links.
servers:
  - url: https://sdn.ac/api/v1
paths:
  /links:
    post:
      summary: Create a short link
      description: Creates a short link. Plain links may be deduplicated; links with expiration, password, preview, or one-time use always get a new code.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLinkRequest'
            examples:
              default:
                value:
                  url: https://example.com/a/very/long/link
                  expiration: 7d
                  burn_after_redirect: false
                  password: optional
                  preview_enabled: true
      responses:
        '201':
          description: Link created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkCreated'
        '200':
          description: Existing deduplicated link returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkCreated'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '429':
          description: Rate limit exceeded
  /links/{code}:
    get:
      summary: Read short link metadata
      parameters:
        - $ref: '#/components/parameters/Code'
      responses:
        '200':
          description: Link metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMetadata'
        '404':
          description: Link not found
        '429':
          description: Rate limit exceeded
  /links/{code}/security:
    get:
      summary: Check a link before redirection
      description: Returns safety signals, expiration, report count, and destination visibility. Password-protected links do not expose the destination URL.
      parameters:
        - $ref: '#/components/parameters/Code'
      responses:
        '200':
          description: Security signals
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkSecurity'
        '404':
          description: Link not found
        '429':
          description: Rate limit exceeded
components:
  parameters:
    Code:
      name: code
      in: path
      required: true
      schema:
        type: string
        pattern: '^[A-Za-z0-9]+$'
      example: 7Hk2Qx
  schemas:
    CreateLinkRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
        expiration:
          type: string
          enum: [never, 24h, 7d, 30d, custom]
          default: never
        custom_expires_at:
          type: string
          format: date-time
          nullable: true
        burn_after_redirect:
          type: boolean
          default: false
        password:
          type: string
          minLength: 4
          maxLength: 255
          nullable: true
        preview_enabled:
          type: boolean
          default: false
    LinkCreated:
      type: object
      properties:
        code:
          type: string
          example: 7Hk2Qx
        short_url:
          type: string
          example: https://sdn.ac/7Hk2Qx
        original_url:
          type: string
          example: https://example.com/a/very/long/link
        expires_at:
          type: string
          format: date-time
          nullable: true
        burn_after_redirect:
          type: boolean
        password_protected:
          type: boolean
        preview_enabled:
          type: boolean
    LinkMetadata:
      type: object
      properties:
        code:
          type: string
        original_url:
          type: string
          nullable: true
          description: Omitted when the link is password-protected.
        visits:
          type: integer
        expires_at:
          type: string
          format: date-time
          nullable: true
        burn_after_redirect:
          type: boolean
        password_protected:
          type: boolean
        preview_enabled:
          type: boolean
    LinkSecurity:
      type: object
      properties:
        code:
          type: string
        short_url:
          type: string
        destination_url:
          type: string
          nullable: true
        destination_available:
          type: boolean
        phishing_status:
          type: string
          enum: [not_flagged, flagged]
        reported:
          type: boolean
        reports_count:
          type: integer
        expired:
          type: boolean
        expires_at:
          type: string
          format: date-time
          nullable: true
        password_protected:
          type: boolean
        burn_after_redirect:
          type: boolean
        preview_enabled:
          type: boolean
        warnings:
          type: array
          items:
            type: string
            enum:
              - phishing_flagged
              - reported_by_users
              - expired
              - destination_hidden_password_required
              - one_time_link
    ValidationError:
      type: object
      properties:
        message:
          type: string
        errors:
          type: object
