openapi: 3.1.0
info:
  title: PulseDeck API
  version: 1.0.0
  description: Scoped API for workspace metadata, normalized performance metrics, integration health, idempotent ingestion and sync, leads, and signed webhooks.
servers:
  - url: https://pulsedeck-kappa.vercel.app/api/v1
security:
  - bearerAuth: []
paths:
  /workspace:
    get:
      summary: Read workspace settings
      x-required-scope: workspace:read
      responses:
        "200":
          description: Workspace metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Workspace"
        "401": { $ref: "#/components/responses/Unauthorized" }
  /metrics:
    get:
      summary: Read normalized metrics and rollups
      x-required-scope: metrics:read
      parameters:
        - in: query
          name: range
          schema: { type: integer, enum: [7, 30, 90, 365], default: 30 }
      responses:
        "200":
          description: Normalized metric points and rollups
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403":
          description: Scope missing or plan history exceeded
  /integrations:
    get:
      summary: Read integration capability and health
      x-required-scope: integrations:read
      responses:
        "200":
          description: Provider capability and safe connection metadata
        "401": { $ref: "#/components/responses/Unauthorized" }
  /ingest/{provider}:
    post:
      summary: Ingest normalized daily metrics
      description: Upserts submitted metric grains without deleting unrelated campaigns or dimensions. Send a unique Idempotency-Key for safe retries.
      x-required-scope: metrics:write
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            type: string
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rows]
              properties:
                rows:
                  type: array
                  maxItems: 5000
                  items:
                    $ref: "#/components/schemas/MetricRow"
      responses:
        "202":
          description: Accepted and stored
        "400":
          description: Invalid row or missing idempotency key
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429":
          description: Rate limit exceeded
  /integrations/{provider}/sync:
    post:
      summary: Request an activated provider sync
      description: Starts an idempotent provider-native sync when the provider app, account, and transport are active.
      x-required-scope: integrations:write
      parameters:
        - in: path
          name: provider
          required: true
          schema: { type: string }
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string, maxLength: 128 }
      responses:
        "202": { description: Sync accepted and rows stored }
        "409": { description: Provider activation or account requirement not satisfied }
        "501": { description: Provider uses normalized ingestion rather than native sync }
        "503": { description: Provider application credentials not configured }
  /leads:
    get:
      summary: List workspace leads
      x-required-scope: leads:read
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        "200": { description: Workspace-scoped lead list }
    post:
      summary: Create a workspace lead
      x-required-scope: leads:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "201": { description: Lead created and lead.created event emitted }
        "400": { description: Validation failed }
  /events:
    post:
      summary: Emit a workspace automation event
      description: Idempotently publishes a supported signed event to matching webhook subscriptions.
      x-required-scope: events:write
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventInput" }
      responses:
        "202": { description: Event accepted }
        "400": { description: Event type or idempotency key invalid }
  /webhooks:
    get:
      summary: List webhook subscriptions and available event types
      x-required-scope: webhooks:manage
      responses:
        "200": { description: Safe webhook subscription metadata }
    post:
      summary: Create a signed webhook subscription
      description: The whsec signing secret is returned once and must be stored immediately.
      x-required-scope: webhooks:manage
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookInput" }
      responses:
        "201": { description: Subscription created; one-time secret returned }
        "400": { description: Event or public HTTPS URL validation failed }
  /webhooks/{id}:
    delete:
      summary: Disable and remove a webhook subscription
      x-required-scope: webhooks:manage
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Subscription removed }
        "404": { description: Subscription not found in this workspace }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: PulseDeck API key
  responses:
    Unauthorized:
      description: Invalid, expired, or revoked API key
  schemas:
    Workspace:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        plan: { type: string, enum: [free, growth, agency] }
        currency: { type: string, example: USD }
        timezone: { type: string, example: Europe/London }
        retentionDays: { type: integer }
    MetricRow:
      type: object
      required: [date, campaign, impressions, clicks, spend, conversions, revenue]
      properties:
        date: { type: string, format: date }
        campaign: { type: string, maxLength: 120 }
        accountId: { type: string, maxLength: 160 }
        campaignId: { type: string, maxLength: 160 }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
        impressions: { type: integer, minimum: 0 }
        clicks: { type: integer, minimum: 0 }
        spend: { type: number, minimum: 0 }
        conversions: { type: integer, minimum: 0 }
        revenue: { type: number, minimum: 0 }
        reach: { type: integer, minimum: 0 }
        linkClicks: { type: integer, minimum: 0 }
        engagements: { type: integer, minimum: 0 }
        videoViews: { type: integer, minimum: 0 }
        placement: { type: string, maxLength: 120 }
        device: { type: string, maxLength: 120 }
        geography: { type: string, maxLength: 120 }
        attributionWindow: { type: string, maxLength: 120 }
    EventInput:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum:
            [
              workspace.created,
              workspace.updated,
              integration.connected,
              integration.disconnected,
              integration.reauth_required,
              sync.started,
              sync.completed,
              sync.failed,
              anomaly.detected,
              recommendation.created,
              campaign.approval_requested,
              recommendation.approval_requested,
              conversion.import_requested,
              privacy.deletion_requested,
              lead.created,
              subscription.payment_failed
            ]
        data: { type: object, additionalProperties: true }
    LeadInput:
      type: object
      required: [name, email, message]
      properties:
        name: { type: string, maxLength: 100 }
        email: { type: string, format: email }
        company: { type: string, maxLength: 120 }
        message: { type: string, maxLength: 2000 }
        source: { type: string, maxLength: 80, default: api }
    WebhookInput:
      type: object
      required: [url, events]
      properties:
        url:
          { type: string, format: uri, description: Public HTTPS URL without embedded credentials }
        description: { type: string, maxLength: 160 }
        events:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            enum:
              [
                workspace.created,
                workspace.updated,
                integration.connected,
                integration.disconnected,
                integration.reauth_required,
                sync.started,
                sync.completed,
                sync.failed,
                anomaly.detected,
                recommendation.created,
                campaign.approval_requested,
                recommendation.approval_requested,
                conversion.import_requested,
                privacy.deletion_requested,
                lead.created,
                subscription.payment_failed
              ]
