openapi: 3.1.0
info:
  title: DropLink API
  version: "1.0"
  description: >
    Programmatic access to DropLink checkout links and analytics.
    Available on the Growth plan. Tokens are created in Settings → API tokens
    and carry scopes (links:read, links:write, analytics:read).
  contact:
    url: https://zerglo.com/apps/droplink/support
servers:
  - url: https://droplink.zerglo.workers.dev/api/v1
security:
  - bearerAuth: []
paths:
  /links:
    get:
      summary: List links
      description: Requires the links:read scope.
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
        - { name: status, in: query, schema: { type: string, enum: [active, paused, archived, all], default: all } }
      responses:
        "200":
          description: Paginated list of links with items and config.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/LinkWithDetails" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      summary: Create a link
      description: >
        Requires the links:write scope. Plan rules are enforced per request:
        discounts/UTMs/notes/multi-product need Starter+, and the plan's
        active-link limit applies.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateLinkBody" }
      responses:
        "201":
          description: The created link with items and config.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/LinkWithDetails" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409":
          description: Active-link limit reached for the plan.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /links/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string }, description: Link ULID }
    get:
      summary: Get a link
      description: Requires the links:read scope.
      responses:
        "200":
          description: The link with items and config.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/LinkWithDetails" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      summary: Update a link
      description: >
        Requires the links:write scope. Send any subset of fields; omitted
        fields keep their current values. isActive toggles active/paused.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateLinkBody" }
      responses:
        "200":
          description: The updated link.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/LinkWithDetails" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      summary: Delete a link
      description: >
        Requires the links:write scope. Permanently removes the link and its
        analytics history — prefer pausing via PUT isActive=false to keep data.
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /analytics/overview:
    get:
      summary: Aggregate analytics across all links
      description: >
        Requires the analytics:read scope. Conversion fields are null on plans
        without conversion tracking.
      responses:
        "200":
          description: Totals across all links.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/AnalyticsOverview" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /analytics/links/{id}:
    get:
      summary: Analytics for one link
      description: Requires the analytics:read scope.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date }, description: ISO date lower bound }
        - { name: to, in: query, schema: { type: string, format: date }, description: ISO date upper bound }
      responses:
        "200":
          description: Click and (plan-permitting) conversion stats for the link.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Success"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/LinkAnalytics" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API token from Settings → API tokens (shown once at creation).
  responses:
    BadRequest:
      description: Validation error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, invalid, expired, or revoked token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Token lacks the required scope or the plan doesn't allow the feature.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Success:
      type: object
      properties:
        success: { type: boolean, const: true }
    Error:
      type: object
      properties:
        success: { type: boolean, const: false }
        error: { type: string }
    Pagination:
      type: object
      properties:
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
    Link:
      type: object
      properties:
        id: { type: string, description: ULID }
        merchant_id: { type: string }
        short_code: { type: string }
        title: { type: string }
        status: { type: string, enum: [active, paused, archived] }
        is_subscription: { type: integer, enum: [0, 1] }
        destination_type: { type: string, enum: [cart_permalink, storefront_api] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    LinkItem:
      type: object
      properties:
        id: { type: string }
        link_id: { type: string }
        shopify_variant_id: { type: string }
        shopify_product_title: { type: string }
        shopify_product_image_url: { type: [string, "null"] }
        quantity: { type: integer }
        selling_plan_id: { type: [string, "null"] }
        selling_plan_name: { type: [string, "null"] }
    LinkConfig:
      type: [object, "null"]
      properties:
        link_id: { type: string }
        discount_code: { type: [string, "null"] }
        utm_source: { type: [string, "null"] }
        utm_medium: { type: [string, "null"] }
        utm_campaign: { type: [string, "null"] }
        custom_note: { type: [string, "null"] }
    LinkWithDetails:
      type: object
      properties:
        link: { $ref: "#/components/schemas/Link" }
        items:
          type: array
          items: { $ref: "#/components/schemas/LinkItem" }
        config: { $ref: "#/components/schemas/LinkConfig" }
    CreateLinkItem:
      type: object
      required: [productId, variantId, quantity]
      properties:
        productId: { type: string, description: Shopify Product GID }
        variantId: { type: string, description: Shopify ProductVariant GID }
        quantity: { type: integer, minimum: 1 }
        productTitle: { type: string }
        productImageUrl: { type: string }
        sellingPlanId: { type: string, description: Makes this a subscription link }
        sellingPlanName: { type: string }
    CreateLinkBody:
      type: object
      required: [name, items]
      properties:
        name: { type: string }
        items:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/CreateLinkItem" }
        discountCode: { type: string }
        utmSource: { type: string }
        utmMedium: { type: string }
        utmCampaign: { type: string }
        customNote: { type: string }
    UpdateLinkBody:
      type: object
      properties:
        name: { type: string }
        isActive: { type: boolean, description: true activates, false pauses }
        items:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/CreateLinkItem" }
        discountCode: { type: string }
        utmSource: { type: string }
        utmMedium: { type: string }
        utmCampaign: { type: string }
        customNote: { type: string }
    AnalyticsOverview:
      type: object
      properties:
        totalClicks: { type: integer }
        uniqueClicks: { type: integer }
        totalConversions: { type: [integer, "null"], description: null without conversion tracking }
        totalRevenue: { type: [number, "null"], description: null without conversion tracking }
        period: { type: string, const: all_time }
    LinkAnalytics:
      type: object
      properties:
        linkId: { type: string }
        linkTitle: { type: string }
        clicks: { type: integer }
        uniqueClicks: { type: integer }
        conversions: { type: [integer, "null"], description: null without conversion tracking }
        revenue: { type: [number, "null"], description: null without conversion tracking }
        period: { type: string, enum: [all_time, filtered] }
        dateRange:
          type: [object, "null"]
          properties:
            from: { type: [string, "null"] }
            to: { type: [string, "null"] }
