openapi: 3.1.0
info:
  title: GLV Links API
  version: 1.8.0
  description: Scoped REST API for links, analytics, domains, usage, conversions, and outbound webhooks.
servers:
  - url: https://links.glvinfotechsolutions.com/api/v1
security:
  - bearerAuth: []
paths:
  /links:
    get:
      summary: List links
      parameters:
        - { in: query, name: cursor, schema: { type: integer, minimum: 0 } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200': { description: Cursor-paginated links }
        default: { $ref: '#/components/responses/Error' }
    post:
      summary: Create a link idempotently
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LinkInput' }
      responses:
        '201':
          description: Link created
          headers: { ETag: { $ref: '#/components/headers/ETag' } }
        '409': { description: Idempotency conflict or request in progress }
        default: { $ref: '#/components/responses/Error' }
  /links/{id}:
    parameters:
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get a link
      responses:
        '200':
          description: Link
          headers: { ETag: { $ref: '#/components/headers/ETag' } }
        '404': { description: Not found }
        default: { $ref: '#/components/responses/Error' }
    put:
      summary: Replace mutable link settings
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/LinkInput' } } }
      responses:
        '200':
          description: Updated link
          headers: { ETag: { $ref: '#/components/headers/ETag' } }
        '412': { description: Resource version mismatch }
        default: { $ref: '#/components/responses/Error' }
    patch:
      summary: Partially update a link
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/LinkPatch' } } }
      responses:
        '200':
          description: Updated link
          headers: { ETag: { $ref: '#/components/headers/ETag' } }
        '412': { description: Resource version mismatch }
        default: { $ref: '#/components/responses/Error' }
    delete:
      summary: Archive a link
      responses:
        '204': { description: Archived }
        default: { $ref: '#/components/responses/Error' }
  /links/{id}/analytics:
    get:
      summary: Read link analytics
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - { in: query, name: days, schema: { oneOf: [{ type: integer, minimum: 1, maximum: 36500 }, { type: string, const: all }] } }
      responses:
        '200': { description: Analytics }
        default: { $ref: '#/components/responses/Error' }
  /domains:
    get:
      summary: List owned custom domains
      responses:
        '200': { description: Domains and verification state }
        default: { $ref: '#/components/responses/Error' }
    post:
      summary: Add or restore a custom domain idempotently
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hostname]
              properties: { hostname: { type: string, example: go.example.com } }
      responses:
        '201': { description: Domain staged with its TXT verification record }
        default: { $ref: '#/components/responses/Error' }
  /usage:
    get:
      summary: Read current plan usage, limits, and remaining allowance
      responses:
        '200': { description: UTC monthly usage snapshot }
        default: { $ref: '#/components/responses/Error' }
  /webhooks:
    get:
      summary: List outbound webhooks
      responses:
        '200': { description: Webhooks }
        default: { $ref: '#/components/responses/Error' }
    post:
      summary: Create an outbound webhook
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookInput' }
      responses:
        '201': { description: Webhook and one-time signing secret }
        default: { $ref: '#/components/responses/Error' }
  /webhooks/{id}:
    delete:
      summary: Delete an outbound webhook
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '204': { description: Deleted }
        default: { $ref: '#/components/responses/Error' }
  /conversion-goals:
    get:
      summary: List conversion goals
      responses: { '200': { description: Goals }, default: { $ref: '#/components/responses/Error' } }
    post:
      summary: Create a conversion goal
      responses: { '201': { description: Goal created }, default: { $ref: '#/components/responses/Error' } }
  /conversions:
    post:
      summary: Record a conversion idempotently
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Conversion created }
        default: { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: glv_live_* }
  parameters:
    ResourceId: { in: path, name: id, required: true, schema: { type: integer, minimum: 1 } }
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string, minLength: 8, maxLength: 120, pattern: '^[A-Za-z0-9._:-]+$' }
    IfMatch:
      in: header
      name: If-Match
      required: false
      description: ETag returned by the most recent GET. A stale value returns 412.
      schema: { type: string }
  headers:
    ETag:
      description: Current resource version for conditional mutation.
      schema: { type: string }
  responses:
    Error:
      description: Stable error response
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
  schemas:
    LinkMutation:
      type: object
      properties:
        destination_url: { type: string, format: uri }
        slug: { type: string }
        title: { type: string, maxLength: 180 }
        redirect_type: { type: integer, enum: [301, 302, 307, 308] }
        status: { type: string, enum: [active, paused, archived] }
        starts_at: { type: [string, 'null'], format: date-time }
        expires_at: { type: [string, 'null'], format: date-time }
        max_clicks: { type: [integer, 'null'], minimum: 1 }
        rules: { type: array, items: { type: object } }
        variants: { type: array, items: { type: object } }
        tags: { oneOf: [{ type: string }, { type: array, items: { type: string } }] }
    LinkInput:
      allOf:
        - $ref: '#/components/schemas/LinkMutation'
        - type: object
          required: [destination_url]
    LinkPatch:
      $ref: '#/components/schemas/LinkMutation'
    WebhookInput:
      type: object
      required: [endpoint_url, events]
      properties:
        name: { type: string, maxLength: 100 }
        endpoint_url: { type: string, format: uri, pattern: '^https://' }
        events:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            enum: [link.created, link.updated, link.paused, link.clicked, link.limit_reached, domain.verified, subscription.updated, conversion.created]
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object }
            request_id: { type: [string, 'null'] }
