openapi: 3.0.3
info:
  title: AutoWorkx Public API
  version: "1.0.0"
  description: >-
    Server-to-server API for Leads, Campaigns, Analytics, Emails, Account and
    Webhooks. Mirrors PUBLIC-API-V1-GUIDE.md — that document is the
    authoritative narrative reference (including known limitations and a
    verification ledger); this spec is a machine-readable summary of the same
    contract, generated for codegen / "import into Postman or Insomnia" use.
    This API has no CORS and must be called server-to-server, never from
    browser JavaScript.
  contact:
    name: AutoWorkx
    url: https://autoworkx.in
externalDocs:
  description: Full integration guide (auth, scopes, rate limits, webhooks, known limitations)
  url: /resources/api-docs
servers:
  - url: https://api.outbird.dev
    description: Production — same host for every account (custom domain, cutover 2026-08-25).

security:
  - bearerAuth: []

tags:
  - name: Leads
  - name: Campaigns
  - name: Analytics
  - name: Emails
  - name: Account
  - name: Email Accounts
  - name: Webhooks

paths:
  /v1/leads:
    get:
      tags: [Leads]
      summary: List leads
      security: [{ bearerAuth: [leads:read] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: lastKey
          in: query
          description: Opaque pagination cursor from a previous response's `nextCursor`.
          schema: { type: string }
      responses:
        "200":
          description: Paginated list of leads.
          content:
            application/json:
              schema:
                type: object
                properties:
                  leads:
                    type: array
                    items: { $ref: "#/components/schemas/Lead" }
                  count: { type: integer }
                  hasMore: { type: boolean }
                  nextCursor: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Leads]
      summary: Create/dedup leads, optionally linked to a campaign
      security: [{ bearerAuth: [leads:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [leads]
              properties:
                leads:
                  type: array
                  items:
                    type: object
                    required: [email]
                    properties:
                      email: { type: string, format: email }
                      name: { type: string }
                      phone: { type: string }
                      company: { type: string }
                      location: { type: string }
                      linkedin: { type: string }
                      website: { type: string }
                      metadata: { type: object, additionalProperties: true }
                campaignId:
                  type: string
                  description: Optional — link every created/matched lead into this campaign.
      responses:
        "201":
          description: Leads created/deduped.
          content:
            application/json:
              schema:
                type: object
                properties:
                  leads: { type: array, items: { $ref: "#/components/schemas/Lead" } }
                  createdCount: { type: integer }
                  duplicateCount: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/leads/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Leads]
      summary: Get a single lead
      security: [{ bearerAuth: [leads:read] }]
      responses:
        "200":
          description: The lead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lead: { $ref: "#/components/schemas/Lead" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Leads]
      summary: Update a lead
      security: [{ bearerAuth: [leads:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Any Lead field except `email` (immutable dedup key) and `leadId`.
              additionalProperties: true
      responses:
        "200":
          description: Updated lead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lead: { $ref: "#/components/schemas/Lead" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns
      security: [{ bearerAuth: [campaigns:read] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: lastKey
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Paginated list of campaigns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaigns: { type: array, items: { $ref: "#/components/schemas/Campaign" } }
                  count: { type: integer }
                  hasMore: { type: boolean }
                  nextCursor: { type: string, nullable: true }
    post:
      tags: [Campaigns]
      summary: Create a campaign
      security: [{ bearerAuth: [campaigns:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CampaignCreateRequest" }
      responses:
        "201":
          description: Campaign created (`status` is always `"inactive"` on creation).
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/campaigns/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Campaigns]
      summary: Get a single campaign
      security: [{ bearerAuth: [campaigns:read] }]
      responses:
        "200":
          description: The campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Campaigns]
      summary: Update a campaign
      description: >-
        Allowlisted fields only: name, campaignTag, campaignMoto, emailIndex,
        followUpEmailIndex. Schedule/trigger internals are not PATCHable via v1.
      security: [{ bearerAuth: [campaigns:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                campaignTag: { type: string }
                campaignMoto: { type: string, enum: [MARKETING, OUTREACH] }
                emailIndex: { type: string }
                followUpEmailIndex: { type: string }
      responses:
        "200":
          description: Updated campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/campaigns/{id}/start:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Campaigns]
      summary: Start a campaign (real send side-effects)
      description: >-
        Requires `emailIndex` to already be set (via POST/PATCH) or fails with 400.
        Begins drafting/sending real emails to every lead in the campaign.
      security: [{ bearerAuth: [campaigns:start] }]
      responses:
        "200": { description: Campaign start pipeline response (shape not fully standardized). }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/campaigns/{id}/pause:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Campaigns]
      summary: Pause a campaign (status transition only, no cleanup — see known limitations)
      security: [{ bearerAuth: [campaigns:start] }]
      responses:
        "200":
          description: Updated campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/campaigns/{id}/resume:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Campaigns]
      summary: Resume a paused campaign
      security: [{ bearerAuth: [campaigns:start] }]
      responses:
        "200":
          description: Updated campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/campaigns/{id}/stop:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Campaigns]
      summary: Stop a campaign
      security: [{ bearerAuth: [campaigns:start] }]
      responses:
        "200":
          description: Updated campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaign: { $ref: "#/components/schemas/Campaign" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/analytics:
    get:
      tags: [Analytics]
      summary: Account-wide analytics
      security: [{ bearerAuth: [analytics:read] }]
      responses:
        "200":
          description: Aggregated analytics across all campaigns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  analytics: { $ref: "#/components/schemas/Analytics" }
                  emailStats: { type: object, additionalProperties: true }
                  usage:
                    type: object
                    properties:
                      containerCount: { type: integer }
                      leadCount: { type: integer }

  /v1/campaigns/{id}/analytics:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Analytics]
      summary: Per-campaign analytics
      security: [{ bearerAuth: [analytics:read] }]
      responses:
        "200":
          description: Analytics for one campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  analytics: { $ref: "#/components/schemas/Analytics" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/emails:
    get:
      tags: [Emails]
      summary: List sent/scheduled emails
      security: [{ bearerAuth: [emails:read] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: lastKey
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Paginated list of emails.
          content:
            application/json:
              schema:
                type: object
                properties:
                  emails: { type: array, items: { $ref: "#/components/schemas/Email" } }
                  count: { type: integer }
                  hasMore: { type: boolean }
                  nextCursor: { type: string, nullable: true }

  /v1/emails/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Emails]
      summary: Get a single email
      security: [{ bearerAuth: [emails:read] }]
      responses:
        "200":
          description: The email.
          content:
            application/json:
              schema:
                type: object
                properties:
                  email: { $ref: "#/components/schemas/Email" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/emails/send:
    post:
      tags: [Emails]
      summary: Send (or schedule) a one-off email
      security: [{ bearerAuth: [emails:send] }]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Client-generated unique string (e.g. a UUID). The only endpoint with idempotency-key
            support today — a retry with the same key after the first completed returns the
            exact original response verbatim (Idempotency-Replayed: true header), no new email
            sent. Claims expire after 24 hours.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, subject, html]
              properties:
                to: { type: string, format: email }
                subject: { type: string }
                html: { type: string }
                emailIndex: { type: string, description: "Connected mailbox to send from." }
                replyToEmailId: { type: string, description: "Threads this send as a reply." }
                scheduledTimeUTC: { type: string, format: date-time, nullable: true }
                leadId: { type: string, nullable: true }
                tag: { type: string, nullable: true }
      responses:
        "201":
          description: Email sent or scheduled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  emailId: { type: string }
                  to: { type: string }
                  subject: { type: string }
                  scheduledTimeUTC: { type: string, nullable: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409":
          description: A request with this Idempotency-Key is still executing — do not retry yet.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /v1/me:
    get:
      tags: [Account]
      summary: Get account info (connected mailboxes, plan, usage)
      security: [{ bearerAuth: [account:read] }]
      responses:
        "200": { description: Account info. }

  /v1/company:
    post:
      tags: [Account]
      summary: Enrich/refresh company/business context from a URL
      security: [{ bearerAuth: [account:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        "200": { description: Enriched company context. }
        "422": { description: Bad/unreachable URL (user input, not a bug). }
        "502": { description: Genuine scrape failure. }

  /v1/webhooks:
    get:
      tags: [Webhooks]
      summary: List your webhooks
      security: [{ bearerAuth: [webhooks:read] }]
      responses:
        "200":
          description: Your registered webhooks (secret masked).
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
    post:
      tags: [Webhooks]
      summary: Register a webhook
      security: [{ bearerAuth: [webhooks:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, description: "Must be https://" }
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - campaign.started
                      - campaign.paused
                      - campaign.resumed
                      - campaign.stopped
                      - email.sent
                      - email.opened
                      - email.clicked
                      - email.replied
                      - email.bounced
                      - "*"
      responses:
        "201":
          description: Webhook registered — `secret` is shown only this once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhookId: { type: string }
                  url: { type: string }
                  events: { type: array, items: { type: string } }
                  status: { type: string }
                  createdAt: { type: string, format: date-time }
                  secret: { type: string }
                  warning: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/webhooks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Webhooks]
      summary: Remove a webhook
      security: [{ bearerAuth: [webhooks:write] }]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhookId: { type: string }
                  status: { type: string, enum: [deleted] }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "aw_<live|test>_<48 hex chars>"
      description: >-
        Authorization: Bearer <apiKey>. X-Api-Key is accepted as a fallback
        only if Authorization can't be set by your tooling — prefer
        Authorization. There is no cookie/session auth on this API.

  responses:
    BadRequest:
      description: Validation failure.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Unauthorized:
      description: >-
        Missing/malformed/expired/revoked API key, or wrong scope for the route. Produced
        directly by the authentication layer before any endpoint runs, which is why
        it's the one response in this API that does NOT use the standard error envelope below
        (an early rejection at that stage can't return a custom body) — body is always exactly
        `{"message":"Unauthorized"}`.
      content:
        application/json:
          schema: { type: object, properties: { message: { type: string, enum: [Unauthorized] } } }
    TooManyRequests:
      description: >-
        Per-key rate limit exceeded (60 requests/minute default). Includes a `Retry-After`
        header (seconds) — honor it before retrying rather than retrying immediately.
      headers:
        Retry-After:
          schema: { type: integer, example: 60 }
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Resource doesn't exist, or isn't owned by this key's account.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: Action attempted from an incompatible state.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    ErrorResponse:
      description: >-
        Standard error envelope returned by every /v1/* endpoint. error.code is a flat, stable
        string (e.g. LEAD_NOT_FOUND, RATE_LIMITED, INVALID_SENDER) — some codes carry extra
        fields alongside code/message (e.g. INVALID_SENDER includes connectedEmails), read those
        directly off `error`. requestId is also echoed as an X-Request-Id response header on
        every request (success or error). The ONLY exception is the authorizer-level 401/403
        (see the Unauthorized response) which can't use this shape — a platform constraint, not
        an inconsistency in this schema.
      type: object
      required: [error, requestId]
      properties:
        error:
          type: object
          required: [code]
          properties:
            code: { type: string, example: "LEAD_NOT_FOUND" }
            message: { type: string, nullable: true }
          additionalProperties: true
        requestId: { type: string, example: "CqWcPijzoAMEJhw=" }

    Lead:
      type: object
      properties:
        leadId: { type: string }
        name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        company: { type: string }
        location: { type: string }
        linkedin: { type: string }
        website: { type: string }
        leadType: { type: string, enum: [api, manual] }
        metadata: { type: object, additionalProperties: true }
        campaignIds: { type: array, items: { type: string } }
        createdAt: { type: string, format: date-time }
        source: { type: string, nullable: true }

    Campaign:
      type: object
      properties:
        campaignId: { type: string }
        name: { type: string }
        status:
          type: string
          description: >-
            starting/running/active are equivalent "in progress" states today
            — see guide's Campaign lifecycle note. Do not treat them as
            distinct in client logic.
          enum:
            - inactive
            - starting
            - running
            - active
            - paused
            - scheduled
            - completed
            - failed
            - stopped
        campaignType: { type: string, enum: [AI, AUTOMATION] }
        campaignMoto: { type: string, enum: [MARKETING, OUTREACH] }
        campaignTag: { type: string }
        trigger:
          type: object
          properties:
            type: { type: string, enum: [IMMEDIATE, AFTER_HOURS, CUSTOM_DATETIME] }
            delayHours: { type: number }
            startAt: { type: string, format: date-time }
        schedule:
          type: object
          properties:
            days: { type: array, items: { type: string } }
            timezoneType: { type: string }
            timezone: { type: string }
            dailyLimit: { type: integer }
            timeWindow:
              type: object
              properties:
                start: { type: string }
                end: { type: string }
        analytics: { $ref: "#/components/schemas/Analytics" }
        usage:
          type: object
          properties:
            leadCount: { type: integer }
        emailIndex: { type: string, nullable: true }
        followUpEmailIndex: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }

    CampaignCreateRequest:
      type: object
      required: [name]
      properties:
        name: { type: string }
        campaignType: { type: string, enum: [AI, AUTOMATION], default: AUTOMATION }
        campaignMoto: { type: string, enum: [MARKETING, OUTREACH], default: MARKETING }
        campaignTag: { type: string, default: "" }
        trigger:
          type: object
          properties:
            type: { type: string, enum: [IMMEDIATE, AFTER_HOURS, CUSTOM_DATETIME], default: IMMEDIATE }
        schedule:
          type: object
          properties:
            days: { type: array, items: { type: string } }
            dailyLimit: { type: integer, default: 100 }
            timeWindow:
              type: object
              properties:
                start: { type: string, default: "09:00" }
                end: { type: string, default: "18:00" }
        campaignQuestions: { type: object, additionalProperties: true }
        emailIndex:
          type: string
          description: Required before `start` will succeed — can also be set later via PATCH.
        followUpEmailIndex: { type: string }

    Analytics:
      type: object
      properties:
        emailsSent: { type: integer }
        opens: { type: integer }
        uniqueOpens: { type: integer }
        clicks: { type: integer }
        uniqueClicks: { type: integer }
        replies: { type: integer }
        bounces: { type: integer }
        unsubscribes: { type: integer }
        lastActivityAt: { type: string, format: date-time, nullable: true }

    Email:
      type: object
      properties:
        emailId: { type: string }
        to: { type: string, nullable: true }
        cc: { type: string, nullable: true }
        bcc: { type: string, nullable: true }
        replyTo: { type: string, nullable: true }
        from: { type: string, nullable: true }
        subject: { type: string }
        body: { type: string }
        status: { type: string }
        campaignId: { type: string, nullable: true }
        leadId: { type: string, nullable: true }
        tag: { type: string, nullable: true }
        sentAt: { type: string, format: date-time, nullable: true }
        scheduledAt: { type: string, format: date-time, nullable: true }

    Webhook:
      type: object
      properties:
        webhookId: { type: string }
        url: { type: string }
        events: { type: array, items: { type: string } }
        status: { type: string }
        secretPrefix: { type: string }
        createdAt: { type: string, format: date-time }
