openapi: 3.1.0
info:
  title: NameSniper API
  version: "1.0.0"
  description: >
    Programmatic access to NameSniper: check brand name, domain, and social
    handle availability across every supported platform, generate AI name
    ideas, screen trademarks, suggest alternatives, and monitor handles for
    when they drop.


    All endpoints require an API key (Pro or Business plan). Authenticate with
    an `Authorization: Bearer ns_sk_...` header. Generate a key at
    https://namesniper.pro/dashboard/api-keys.


    Every response uses the envelope `{ "success": boolean, "data"?: object,
    "meta"?: object, "error"?: string }`.
  contact:
    name: NameSniper
    url: https://namesniper.pro
  license:
    name: Proprietary
    url: https://namesniper.pro/terms
servers:
  - url: https://namesniper.pro
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Check
    description: Availability checks for names, domains, and social handles
  - name: Generate
    description: AI name generation and alternatives
  - name: Trademark
    description: USPTO trademark screening
  - name: Monitoring
    description: Watch handles and receive notifications
  - name: Webhooks
    description: Manage webhook endpoints for monitoring events
  - name: Account
    description: Plan and usage information

paths:
  /api/v1/check:
    post:
      tags: [Check]
      summary: All-in-one check
      description: Check a name across domains and social platforms, with optional brand score and trademark screening.
      operationId: checkAll
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Brand name or handle to check.
                  example: acmerobotics
                platforms:
                  type: array
                  items: { $ref: '#/components/schemas/PlatformName' }
                  description: Social platforms to check. Defaults to every supported platform (Telegram must be requested explicitly). Unknown ids return 400 with the valid list.
                domains:
                  type: array
                  items: { $ref: '#/components/schemas/TLD' }
                  description: TLDs to check. Defaults to com, org, net, app, dev, tech, io, co, ai.
                brandScore:
                  type: boolean
                  default: true
                  description: Include brand score analysis.
                trademark:
                  type: boolean
                  default: true
                  description: Include USPTO trademark screening.
      responses:
        '200':
          description: Combined results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      name: { type: string }
                      domains: { $ref: '#/components/schemas/DomainCheckResponse' }
                      social: { $ref: '#/components/schemas/UsernameCheckResponse' }
                      brandScore: { $ref: '#/components/schemas/BrandScore' }
                      trademark: { $ref: '#/components/schemas/TrademarkCheckResponse' }
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/check/social:
    post:
      tags: [Check]
      summary: Check social handles
      operationId: checkSocial
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  example: acmerobotics
                platforms:
                  type: array
                  items: { $ref: '#/components/schemas/PlatformName' }
                  description: Defaults to every supported platform (Telegram must be requested explicitly). Unknown ids return 400 with the valid list.
      responses:
        '200':
          description: Social availability results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - type: object
                        properties:
                          name: { type: string }
                      - $ref: '#/components/schemas/UsernameCheckResponse'
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/check/domains:
    post:
      tags: [Check]
      summary: Check domain availability
      operationId: checkDomains
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Domain name without TLD.
                  example: acmerobotics
                tlds:
                  type: array
                  items: { $ref: '#/components/schemas/TLD' }
                  description: TLDs to check. Defaults to com, org, net, app, dev, tech, io, co, ai.
                details:
                  type: boolean
                  default: false
                  description: >-
                    Attach a `registration` record (registrar, dates, status, drop stage) to each
                    taken TLD, read from the registry. Adds up to a few seconds. Registrant identity
                    is usually redacted by registries; some ccTLDs publish no registry data and the
                    field is then omitted.
      responses:
        '200':
          description: Domain availability results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - type: object
                        properties:
                          name: { type: string }
                      - $ref: '#/components/schemas/DomainCheckResponse'
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/brand-score:
    post:
      tags: [Generate]
      summary: Brand name analysis
      operationId: brandScore
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: acmerobotics }
      responses:
        '200':
          description: Brand score.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - type: object
                        properties:
                          name: { type: string }
                      - $ref: '#/components/schemas/BrandScore'
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/alternatives:
    post:
      tags: [Generate]
      summary: Brand name alternatives
      operationId: alternatives
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: acmerobotics }
                count:
                  type: integer
                  default: 8
                  description: Number of alternatives to return.
      responses:
        '200':
          description: Alternative name suggestions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      name: { type: string }
                      alternatives:
                        type: array
                        items: { $ref: '#/components/schemas/BrandAlternative' }
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/generate:
    post:
      tags: [Generate]
      summary: AI name generation
      operationId: generate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description:
                  type: string
                  minLength: 3
                  description: Business or project description.
                  example: A robotics startup building warehouse automation arms
                count:
                  type: integer
                  default: 10
                  minimum: 1
                  maximum: 30
                  description: Values outside the range are clamped, not rejected.
                style:
                  $ref: '#/components/schemas/GenerationStyle'
      responses:
        '200':
          description: Generated names with .com availability.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      description: { type: string }
                      names:
                        type: array
                        items: { $ref: '#/components/schemas/GeneratedName' }
                      generatedAt: { type: string, format: date-time }
                      cached: { type: boolean }
                      quota: { $ref: '#/components/schemas/AiQuota' }
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/v1/trademark:
    post:
      tags: [Trademark]
      summary: Trademark screening
      operationId: trademark
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: acmerobotics }
                includeVariations:
                  type: boolean
                  default: false
                classes:
                  type: array
                  items: { type: string }
                  description: International trademark classes to focus on.
      responses:
        '200':
          description: Trademark screening result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/TrademarkCheckResponse' }
                  meta: { $ref: '#/components/schemas/Meta' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429':
          description: 'Rate limit exceeded, or trademark screening is temporarily unavailable for this key (`code: trademark_unavailable`). Retry later.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /api/v1/usage:
    get:
      tags: [Account]
      summary: Plan and usage
      operationId: usage
      responses:
        '200':
          description: Current plan limits and usage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      plan: { $ref: '#/components/schemas/PlanName' }
                      limits:
                        type: object
                        properties:
                          checksPerDay:
                            oneOf:
                              - type: integer
                              - type: string
                                enum: [unlimited]
                          aiGenerationsPerMonth:
                            oneOf:
                              - type: integer
                              - type: string
                                enum: [unlimited]
                          apiRequestsPerDay: { type: integer }
                      usage:
                        type: object
                        properties:
                          checksToday: { type: integer }
                          apiRequestsToday: { type: integer }
                          aiGenerationsThisMonth: { type: integer }
                      rateLimit:
                        type: [object, "null"]
                        properties:
                          perMinute: { type: integer }
                          perHour: { type: integer }
                          perDay: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/watch:
    get:
      tags: [Monitoring]
      summary: List watched handles
      operationId: listWatches
      responses:
        '200':
          description: All watches for the authenticated user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      watches:
                        type: array
                        items: { $ref: '#/components/schemas/Watch' }
                  meta:
                    type: object
                    properties:
                      slots: { $ref: '#/components/schemas/Slots' }
                      total: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Monitoring]
      summary: Create watches
      description: Start monitoring a handle across one or more platforms. Instagram, TikTok, and X require Pro or Business.
      operationId: createWatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, platforms]
              properties:
                username: { type: string, example: acmerobotics }
                platforms:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/PlatformName' }
      responses:
        '200':
          description: Created watches (duplicates are skipped).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      created:
                        type: array
                        items: { $ref: '#/components/schemas/Watch' }
                      skipped:
                        type: array
                        items: { $ref: '#/components/schemas/PlatformName' }
                  meta:
                    type: object
                    properties:
                      slots: { $ref: '#/components/schemas/Slots' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/watch/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    patch:
      tags: [Monitoring]
      summary: Pause or resume a watch
      operationId: updateWatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [watching, paused]
      responses:
        '200':
          description: Updated watch.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Watch' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Monitoring]
      summary: Remove a watch
      operationId: deleteWatch
      responses:
        '200':
          description: Watch removed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeleteResult' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      responses:
        '200':
          description: All webhook endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      webhooks:
                        type: array
                        items: { $ref: '#/components/schemas/Webhook' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint
      description: Requires Pro or Business plan. The signing secret is returned only at creation.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  example: https://example.com/hooks/namesniper
                events:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200':
          description: Created webhook (includes the signing secret).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/WebhookWithSecret' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    patch:
      tags: [Webhooks]
      summary: Update a webhook endpoint
      operationId: updateWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/WebhookEvent' }
                active: { type: boolean }
      responses:
        '200':
          description: Updated webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Webhook' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Webhooks]
      summary: Remove a webhook endpoint
      operationId: deleteWebhook
      responses:
        '200':
          description: Webhook removed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeleteResult' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/notifications:
    get:
      tags: [Monitoring]
      summary: List in-app notifications
      operationId: listNotifications
      parameters:
        - name: unread
          in: query
          schema: { type: boolean }
          description: When true, only return unread notifications.
        - name: limit
          in: query
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        '200':
          description: Paginated notifications.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      notifications:
                        type: array
                        items: { $ref: '#/components/schemas/Notification' }
                      pagination:
                        type: object
                        properties:
                          total: { type: integer }
                          limit: { type: integer }
                          offset: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/notifications/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    patch:
      tags: [Monitoring]
      summary: Mark a notification as read
      operationId: markNotificationRead
      responses:
        '200':
          description: Updated notification.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Notification' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/notifications/read-all:
    post:
      tags: [Monitoring]
      summary: Mark all notifications as read
      operationId: markAllNotificationsRead
      responses:
        '200':
          description: Count of notifications marked read.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      markedRead: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ns_sk
      description: "API key issued from the dashboard, sent as `Authorization: Bearer ns_sk_...`."

  parameters:
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string }

  responses:
    BadRequest:
      description: Invalid request (missing field, bad JSON, or invalid value).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Forbidden:
      description: Authenticated but not permitted (revoked key or a feature your plan does not include).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    NotFound:
      description: Resource not found or not owned by the caller.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    RateLimited:
      description: >-
        Rate limit or daily quota exceeded. Inspect the `Retry-After` header for how many
        seconds to wait. Limits are per account, so every key on the account shares them.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }

  schemas:
    ErrorResponse:
      type: object
      properties:
        success: { const: false }
        error:
          type: string
          description: Human-readable message.
        code:
          type: string
          description: Optional machine-readable code on some 429s (for example `trademark_unavailable`, `daily_limit`).
      required: [success, error]

    Meta:
      type: object
      description: Standard response metadata.
      properties:
        plan: { $ref: '#/components/schemas/PlanName' }
        duration:
          type: integer
          description: Server processing time in milliseconds.
        timestamp: { type: string, format: date-time }

    Slots:
      type: object
      properties:
        used: { type: integer }
        limit: { type: integer }

    DeleteResult:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          properties:
            deleted: { const: true }
            id: { type: string }

    PlatformName:
      type: string
      enum:
        - youtube
        - tiktok
        - instagram
        - twitter
        - github
        - npm
        - twitch
        - threads
        - bluesky
        - pinterest
        - snapchat
        - kick
        - linktree
        - roblox
        - telegram

    TLD:
      type: string
      enum:
        - com
        - net
        - org
        - io
        - ai
        - app
        - dev
        - tech
        - cloud
        - software
        - tools
        - page
        - network
        - systems
        - wiki
        - codes
        - co
        - pro
        - biz
        - company
        - agency
        - group
        - team
        - ventures
        - capital
        - global
        - solutions
        - services
        - finance
        - consulting
        - marketing
        - studio
        - online
        - site
        - digital
        - fun
        - design
        - live
        - media
        - art
        - blog
        - space
        - video
        - news
        - photo
        - store
        - shop
        - market
        - style
        - fashion
        - boutique
        - deals
        - sale
        - gg
        - social
        - club
        - games
        - community
        - chat
        - xyz
        - me
        - tv
        - info
        - cc
        - link
        - one
        - website
        - bio
        - life
        - vip

    GenerationStyle:
      type: string
      default: modern
      enum:
        - modern
        - classic
        - tech
        - creative
        - professional
        - playful
        - abstract
        - luxury
        - minimalist

    PlanName:
      type: string
      enum: [guest, free, day_pass, sniper, pro, business]

    WebhookEvent:
      type: string
      enum: [handle_available, handle_change]

    CheckResult:
      type: object
      description: Availability of a single handle on one platform.
      properties:
        platform: { $ref: '#/components/schemas/PlatformName' }
        username: { type: string }
        available: { type: boolean }
        confidence:
          type: number
          description: Confidence in the verdict, 0 to 1.
        error: { type: string }
        invalid:
          type: boolean
          description: Username violates the platform's format rules and can never be registered there.
        loginWallDetected: { type: boolean }
        botDetected: { type: boolean }
        checkedAt: { type: string, format: date-time }
        url: { type: string, format: uri }
      required: [platform, username, available, confidence, checkedAt, url]

    CheckSummary:
      type: object
      properties:
        totalChecks: { type: integer }
        available: { type: integer }
        taken: { type: integer }
        errors: { type: integer }
        uncertain:
          type: integer
          description: Checks that got no verdict either way (low confidence or errored). Social checks only.

    UsernameCheckResponse:
      type: object
      properties:
        username: { type: string }
        results:
          type: array
          items: { $ref: '#/components/schemas/CheckResult' }
        summary: { $ref: '#/components/schemas/CheckSummary' }

    DomainResult:
      type: object
      properties:
        domain: { $ref: '#/components/schemas/TLD' }
        domainName: { type: string }
        available: { type: boolean }
        status:
          type: string
          enum: [available, taken, uncertain]
        checkedAt: { type: string, format: date-time }
        confidence:
          type: number
          description: Confidence level, 0 to 100.
        error: { type: string }
        tier:
          type: integer
          enum: [1, 2, 3]
          description: Which checking tier produced the verdict.
        checkTime:
          type: integer
          description: Time taken in milliseconds.
        registration:
          $ref: '#/components/schemas/DomainRegistration'
      required: [domain, domainName, available, checkedAt]

    DomainRegistration:
      type: object
      description: >-
        Registry registration record for a taken domain. Present only when the request set
        `details: true`, the domain is taken, and the registry publishes registration data.
      properties:
        registrar: { type: string, example: 'MarkMonitor Inc.' }
        registrantOrg:
          type: string
          description: Registrant organisation when the registry publishes it (rare; usually redacted).
        registeredAt: { type: string, format: date-time }
        expiresAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        status:
          type: array
          items: { type: string }
          description: Raw registry status tokens, e.g. "client transfer prohibited".
        nameservers:
          type: array
          items: { type: string }
        dropStage:
          type: string
          enum: [active, expired-grace, redemption, pending-delete]
          description: >-
            Where the name sits in the expiry / drop lifecycle. Omitted when the expiry date is
            unknown. Expired names are usually caught by backorder services within seconds of
            release.
        source: { type: string, enum: [rdap] }
        fetchedAt: { type: string, format: date-time }
      required: [status, nameservers, source, fetchedAt]

    DomainCheckResponse:
      type: object
      properties:
        domainName: { type: string }
        results:
          type: array
          items: { $ref: '#/components/schemas/DomainResult' }
        summary: { $ref: '#/components/schemas/CheckSummary' }

    GeneratedName:
      type: object
      properties:
        name: { type: string }
        explanation: { type: string }
        tags:
          type: array
          items: { type: string }
        confidence: { type: number }
        comAvailable:
          type: boolean
          description: Whether the .com is available (DNS check).
        source:
          type: string
          enum: [ai, fallback]
          description: "'fallback' means the AI was unavailable and a mock was returned; not charged against quota."
      required: [name, explanation, tags, confidence]

    BrandAlternative:
      type: object
      properties:
        name: { type: string }
        pattern: { type: string }
        description: { type: string }
        brandScore:
          type: integer
          description: 0 to 100, higher is more brandable.
        type:
          type: string
          enum: [prefix, suffix, variation, compound]
      required: [name, pattern, description, brandScore, type]

    AiQuota:
      type: object
      description: The caller's AI generation allowance after this request.
      properties:
        used: { type: integer }
        limit:
          type: [integer, "null"]
          description: 'null when the plan has no cap on generations.'
        period:
          type: [string, "null"]
          enum: [day, month, null]
          description: Which window `limit` applies to.
      required: [used, limit, period]

    ScoreComponent:
      type: object
      description: One weighted input to the Ownability Score.
      properties:
        key:
          type: string
          enum: [com, premiumDomains, priorityHandles, otherHandles, trademark]
        weight:
          type: number
          description: Nominal weight before the score is renormalized over the components that had data.
        earned:
          type: number
          description: Points earned, from 0 to `weight`.
        available:
          type: integer
          description: Channels found available. Channel components only.
        checked:
          type: integer
          description: Channels that returned a definitive verdict. Channel components only.
        unverified:
          type: integer
          description: Key handles that could not be verified (plan-locked or low confidence). They earn half credit.
        included:
          type: boolean
          description: Whether this component had data and contributed to the score.
      required: [key, weight, earned, available, checked, unverified, included]

    BrandScore:
      type: object
      description: Ownability Score. Weighted availability across .com, premium TLDs, priority handles, other handles, and USPTO trademark risk.
      properties:
        overall:
          type: integer
          description: 0 to 100. Higher means more of the name is still claimable.
        band:
          type: string
          enum: [wide-open, mostly-open, mixed, contested]
        components:
          type: array
          items: { $ref: '#/components/schemas/ScoreComponent' }
        channelsCounted:
          type: integer
          description: Channels with a verdict that fed the score.
        channelsTotal:
          type: integer
          description: Channels attempted, including gated and errored ones.
        cappedByTrademark:
          type: boolean
          description: True when a high-risk trademark conflict hard-capped the score.
        provisional:
          type: boolean
          description: True while a key handle is unverified, so the score can still move once it is checked.
      required: [overall, band, components, channelsCounted, channelsTotal, cappedByTrademark, provisional]

    TrademarkConflict:
      type: object
      properties:
        mark: { type: string }
        similarity: { type: integer }
        owner: { type: string }
        classes:
          type: array
          items: { type: string }
        status: { type: string }
        riskLevel: { $ref: '#/components/schemas/RiskLevel' }
        serialNumber: { type: string }
        goodsAndServices: { type: string }
        filingStatus: { type: string }

    RiskLevel:
      type: string
      enum: [low, medium, high]

    TrademarkResult:
      type: object
      properties:
        name: { type: string }
        status:
          type: string
          enum: [no-matches, similar-found, unknown]
        confidence: { type: integer }
        riskLevel: { $ref: '#/components/schemas/RiskLevel' }
        details:
          type: object
          additionalProperties: true
        conflicts:
          type: array
          items: { $ref: '#/components/schemas/TrademarkConflict' }
        checkedAt: { type: string, format: date-time }
        source:
          type: string
          enum: [uspto-search, similarity-analysis]
        isAutomatedScreening: { const: true }
        disclaimer: { type: string }

    TrademarkCheckResponse:
      type: object
      properties:
        name: { type: string }
        result: { $ref: '#/components/schemas/TrademarkResult' }
        variations:
          type: array
          items: { $ref: '#/components/schemas/TrademarkResult' }
        summary:
          type: object
          properties:
            overallRisk: { $ref: '#/components/schemas/RiskLevel' }
            recommendAction: { type: string }
            conflictCount: { type: integer }
        disclaimer: { type: string }
        isAutomatedScreening: { const: true }

    Watch:
      type: object
      properties:
        id: { type: string }
        username: { type: string }
        platform: { $ref: '#/components/schemas/PlatformName' }
        status: { type: string, description: "Current watch state, e.g. watching or paused." }
        lastStatus:
          type: [string, "null"]
          description: Last observed availability status.
        lastCheckedAt: { type: [string, "null"], format: date-time }
        lastChangedAt: { type: [string, "null"], format: date-time }
        checkCount: { type: integer }
        createdAt: { type: string, format: date-time }
      required: [id, username, platform, status, createdAt]

    Webhook:
      type: object
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEvent' }
        active: { type: boolean }
        failCount: { type: integer }
        lastError: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    WebhookWithSecret:
      type: object
      description: Returned only at creation. The secret signs the HMAC-SHA256 payload header and is never shown again.
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEvent' }
        active: { type: boolean }
        secret: { type: string }
        createdAt: { type: string, format: date-time }

    Notification:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        title: { type: string }
        body: { type: string }
        read: { type: boolean }
        metadata:
          type: [object, "null"]
          additionalProperties: true
        watchId: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
