openapi: 3.1.0
info:
  title: 4ul.ink API
  version: 1.0.0
  description: Create, manage, and analyze links within the workspace assigned to an API token.
servers:
  - url: https://4ul.ink
security:
  - bearerAuth: []
paths:
  /api/v1/links:
    get:
      summary: List links
      description: Requires links:read.
      parameters:
        - {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
        - {name: per_page, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 25}}
      responses:
        "200":
          description: Paginated links
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Link"}}
                  meta: {$ref: "#/components/schemas/Pagination"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
    post:
      summary: Create a link
      description: Requires links:write.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: A unique 8-128 character key retained for 24 hours.
          schema: {type: string, minLength: 8, maxLength: 128}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination_url]
              properties:
                destination_url: {type: string, format: uri, maxLength: 2048}
                slug: {type: string, minLength: 3, maxLength: 80}
                title: {type: string, maxLength: 190}
      responses:
        "201":
          description: Link created
          content:
            application/json:
              schema: {$ref: "#/components/schemas/LinkEnvelope"}
        "409": {$ref: "#/components/responses/Conflict"}
        "422": {$ref: "#/components/responses/ValidationError"}
  /api/v1/links/{id}:
    parameters:
      - {name: id, in: path, required: true, schema: {type: integer, minimum: 1}}
    patch:
      summary: Update a link
      description: Requires links:write. Only supplied fields are changed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                destination_url: {type: string, format: uri, maxLength: 2048}
                slug: {type: string, minLength: 3, maxLength: 80}
                title: {type: [string, "null"], maxLength: 190}
                is_active: {type: boolean}
      responses:
        "200":
          description: Updated link
          content:
            application/json:
              schema: {$ref: "#/components/schemas/LinkEnvelope"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
        "422": {$ref: "#/components/responses/ValidationError"}
    delete:
      summary: Disable a link
      description: Requires links:write. The row and existing analytics are retained.
      responses:
        "200": {description: Link disabled}
        "404": {$ref: "#/components/responses/NotFound"}
  /api/v1/links/{id}/analytics:
    get:
      summary: Read link analytics
      description: Requires analytics:read. Object access is restricted to the token workspace.
      parameters:
        - {name: id, in: path, required: true, schema: {type: integer, minimum: 1}}
        - {name: period, in: query, schema: {type: string, enum: ["7", "30", "90", custom], default: "30"}}
        - {name: from, in: query, schema: {type: string, format: date}}
        - {name: to, in: query, schema: {type: string, format: date}}
      responses:
        "200": {description: Aggregated analytics}
        "404": {$ref: "#/components/responses/NotFound"}
  /api/v1/conversions:
    post:
      summary: Record an attributed conversion
      description: Requires links:write and conversion tracking entitlement.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [click_token, event, external_id]
              properties:
                click_token: {type: string}
                event: {type: string, enum: [lead, sale, refund]}
                external_id: {type: string, maxLength: 190}
                amount: {type: number, minimum: 0}
                currency: {type: string, pattern: "^[A-Z]{3}$"}
      responses:
        "201": {description: Conversion recorded}
        "409": {$ref: "#/components/responses/Conflict"}
        "422": {$ref: "#/components/responses/ValidationError"}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 4ul_live token
  schemas:
    Link:
      type: object
      required: [id, slug, short_url, destination_url, click_count, is_active, created_at]
      properties:
        id: {type: integer}
        slug: {type: string}
        short_url: {type: string, format: uri}
        destination_url: {type: string, format: uri}
        title: {type: [string, "null"]}
        click_count: {type: integer}
        is_active: {type: boolean}
        created_at: {type: string}
    LinkEnvelope:
      type: object
      required: [data]
      properties:
        data: {$ref: "#/components/schemas/Link"}
    Pagination:
      type: object
      required: [page, per_page, total, total_pages]
      properties:
        page: {type: integer}
        per_page: {type: integer}
        total: {type: integer}
        total_pages: {type: integer}
    Error:
      type: object
      required: [error]
      properties:
        error: {type: string}
        code: {type: string}
  responses:
    Unauthorized:
      description: Invalid bearer token
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    NotFound:
      description: Resource was not found in the token workspace
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    Conflict:
      description: Resource conflict
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    ValidationError:
      description: Request validation failed
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    RateLimited:
      description: Rate or monthly usage limit reached
      headers:
        Retry-After: {schema: {type: integer}}
        RateLimit-Limit: {schema: {type: integer}}
        RateLimit-Remaining: {schema: {type: integer}}
        RateLimit-Reset: {schema: {type: integer}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
