openapi: 3.0.3
info:
  title: LeadReachIO API
  version: "v1"
  description: |
    External contract for LeadReachIO (realtor-focused property intelligence + compliant outreach + tracking).

servers:
  - url: https://api.gwt.io

tags:
  - name: Health
  - name: Auth
  - name: Entitlements
  - name: Properties
  - name: Campaigns
  - name: Leads
  - name: Reporting

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque

  schemas:
    ErrorResponse:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          enum: [false]
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - not_found
                - validation_error
                - conflict
                - rate_limited
                - internal_error
            message:
              type: string

    OkResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          type: object

    UserProfile:
      type: object
      required:
        - fullName
        - email
        - phoneNumber
        - realtorLicenseNumber
        - state
      properties:
        fullName:
          type: string
        email:
          type: string
          format: email
        phoneNumber:
          type: string
          description: E.164 preferred.
        realtorLicenseNumber:
          type: string
        state:
          type: string
          description: Two-letter US state code preferred.
        brokerageName:
          type: string
          nullable: true

    SubscriptionStatus:
      type: string
      enum: [active, expired, trial, none]

    SessionInfo:
      type: object
      required: [sessionToken, expiresAt, subscriptionStatus]
      properties:
        sessionToken:
          type: string
        expiresAt:
          type: string
          format: date-time
        subscriptionStatus:
          $ref: "#/components/schemas/SubscriptionStatus"

    InvitationAcceptRequest:
      type: object
      required: [invitationCode, password, profile]
      properties:
        invitationCode:
          type: string
        password:
          type: string
          minLength: 8
        profile:
          $ref: "#/components/schemas/UserProfile"

    SignInRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string

    VerifySubscriptionRequest:
      oneOf:
        - $ref: "#/components/schemas/VerifySubscriptionRequestIos"
        - $ref: "#/components/schemas/VerifySubscriptionRequestAndroid"

    VerifySubscriptionRequestIos:
      type: object
      required: [platform, transactionId]
      properties:
        platform:
          type: string
          enum: [ios]
        transactionId:
          type: string
          description: |
            A StoreKit transaction identifier that belongs to the user. This may be an original transaction identifier.
        environment:
          type: string
          enum: [production, sandbox]
          description: Optional hint for which Apple environment to verify against.

    VerifySubscriptionRequestAndroid:
      type: object
      required: [platform, packageName, purchaseToken]
      properties:
        platform:
          type: string
          enum: [android]
        packageName:
          type: string
          description: Android application package name.
        purchaseToken:
          type: string
          description: Google Play purchase token for the subscription.

    PropertyIdentifyRequest:
      type: object
      description: Provide at least one hint (address or geo).
      properties:
        address:
          type: string
        latitude:
          type: number
        longitude:
          type: number
        radiusMeters:
          type: integer
          minimum: 10
          maximum: 500
      additionalProperties: false

    PropertyCandidate:
      type: object
      required: [propertyId, address]
      properties:
        propertyId:
          type: string
        address:
          type: string
        confidence:
          type: number
          minimum: 0
          maximum: 1

    PropertyCard:
      type: object
      required: [propertyId, address]
      properties:
        propertyId:
          type: string
        address:
          type: string
        beds:
          type: number
          nullable: true
        baths:
          type: number
          nullable: true
        squareFeet:
          type: integer
          nullable: true
        lotSize:
          type: string
          nullable: true
        yearBuilt:
          type: integer
          nullable: true
        propertyType:
          type: string
          nullable: true
        purchaseDate:
          type: string
          format: date
          nullable: true
        lastSalePrice:
          type: number
          nullable: true
        valueEstimateLow:
          type: number
          nullable: true
        valueEstimateHigh:
          type: number
          nullable: true
        occupancySignal:
          type: string
          nullable: true
          description: Owner-occupied vs absentee indicator where available.
        mlsStatus:
          type: string
          nullable: true
          description: Active/expired/off-market indicator where available.
        ownerNames:
          type: array
          items:
            type: string
          nullable: true
        ownerContact:
          type: object
          nullable: true
          description: Sensitive; may be masked/omitted based on availability/confidence.
          properties:
            phone:
              type: string
              nullable: true
            email:
              type: string
              format: email
              nullable: true
            confidence:
              type: number
              minimum: 0
              maximum: 1
              nullable: true

    CampaignPreset:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
        channels:
          type: array
          items:
            type: string
            enum: [sms, email]

    CampaignPreviewRequest:
      type: object
      required: [propertyId, presetId]
      properties:
        propertyId:
          type: string
        presetId:
          type: string

    CampaignPreview:
      type: object
      required: [campaignName, sms, email]
      properties:
        campaignName:
          type: string
        sms:
          type: object
          required: [body, locked]
          properties:
            body:
              type: string
            locked:
              type: array
              items:
                type: string
              description: Segments that are compliance-critical and cannot be removed.
        email:
          type: object
          required: [subject, body, locked]
          properties:
            subject:
              type: string
            body:
              type: string
            locked:
              type: array
              items:
                type: string

    CampaignLaunchRequest:
      type: object
      required: [propertyId, presetId, channels]
      properties:
        propertyId:
          type: string
        presetId:
          type: string
        channels:
          type: array
          items:
            type: string
            enum: [sms, email]
        overrides:
          type: object
          nullable: true
          properties:
            smsBody:
              type: string
              nullable: true
            emailSubject:
              type: string
              nullable: true
            emailBody:
              type: string
              nullable: true

    LeadStatus:
      type: string
      enum: [new, warm, hot, closed, do_not_contact]

    LeadSummary:
      type: object
      required: [leadId, propertyId, status, createdAt, lastActivityAt]
      properties:
        leadId:
          type: string
        propertyId:
          type: string
        propertyAddress:
          type: string
          nullable: true
        ownerNames:
          type: array
          nullable: true
          items:
            type: string
        status:
          $ref: "#/components/schemas/LeadStatus"
        notes:
          type: string
          nullable: true
        lastCampaignId:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        lastActivityAt:
          type: string
          format: date-time

    LeadDetail:
      allOf:
        - $ref: "#/components/schemas/LeadSummary"

    Message:
      type: object
      required: [messageId, channel, direction, to, from, body, status, createdAt]
      properties:
        messageId:
          type: string
        channel:
          type: string
          enum: [sms, email]
        direction:
          type: string
          enum: [outbound, inbound]
        to:
          type: string
        from:
          type: string
        subject:
          type: string
          nullable: true
        body:
          type: string
        status:
          type: string
          enum: [queued, sending, sent, delivered, failed, received]
        createdAt:
          type: string
          format: date-time
        sentAt:
          type: string
          format: date-time
          nullable: true

paths:
  /v1/health:
    get:
      tags: [Health]
      summary: Health check
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    enum: [true]

  /v1/email/unsubscribe:
    get:
      tags: [Campaigns]
      summary: Email unsubscribe (recipient-facing)
      description: |
        Public unsubscribe endpoint for email recipients. This endpoint does not require authentication.
        When invoked, the recipient email address is suppressed for future sends.
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Unsubscribed
          content:
            text/html:
              schema:
                type: string
        "400":
          description: Invalid request
          content:
            text/html:
              schema:
                type: string
        "404":
          description: Not found
          content:
            text/html:
              schema:
                type: string

  /v1/auth/invitations/accept:
    post:
      tags: [Auth]
      summary: Accept invitation and create account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvitationAcceptRequest"
      responses:
        "200":
          description: Account created
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [created]
                    properties:
                      created:
                        type: boolean
                        enum: [true]
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invitation invalid/used/expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/auth/sessions:
    post:
      tags: [Auth]
      summary: Create a user session (sign-in)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignInRequest"
      responses:
        "200":
          description: Session created
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: "#/components/schemas/SessionInfo"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/auth/sessions/current:
    delete:
      tags: [Auth]
      summary: End current session (sign-out)
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Signed out
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/me:
    get:
      tags: [Auth]
      summary: Get current user profile + subscription status
      security:
        - bearerAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [profile, subscriptionStatus]
                    properties:
                      profile:
                        $ref: "#/components/schemas/UserProfile"
                      subscriptionStatus:
                        $ref: "#/components/schemas/SubscriptionStatus"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/entitlements/verify:
    post:
      tags: [Entitlements]
      summary: Verify a mobile-store subscription token and activate entitlement
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifySubscriptionRequest"
      responses:
        "200":
          description: Entitlement state
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [subscriptionStatus]
                    properties:
                      subscriptionStatus:
                        $ref: "#/components/schemas/SubscriptionStatus"
                      expiresAt:
                        type: string
                        format: date-time
                        nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/properties/identify:
    post:
      tags: [Properties]
      summary: Identify candidate properties from an address or geo hint
      description: Subscription required.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PropertyIdentifyRequest"
      responses:
        "200":
          description: Candidate properties
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [candidates]
                    properties:
                      candidates:
                        type: array
                        items:
                          $ref: "#/components/schemas/PropertyCandidate"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/properties/{propertyId}:
    get:
      tags: [Properties]
      summary: Get property intelligence card
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: propertyId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Property card
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: "#/components/schemas/PropertyCard"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/campaign-presets:
    get:
      tags: [Campaigns]
      summary: List preset campaigns
      description: Subscription required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Preset list
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [presets]
                    properties:
                      presets:
                        type: array
                        items:
                          $ref: "#/components/schemas/CampaignPreset"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/campaigns/preview:
    post:
      tags: [Campaigns]
      summary: Preview a preset campaign for a property
      description: Subscription required.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CampaignPreviewRequest"
      responses:
        "200":
          description: Preview
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: "#/components/schemas/CampaignPreview"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/campaigns:
    post:
      tags: [Campaigns]
      summary: Launch a campaign
      description: Subscription required.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CampaignLaunchRequest"
      responses:
        "200":
          description: Launched
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [campaignId]
                    properties:
                      campaignId:
                        type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/leads:
    get:
      tags: [Leads]
      summary: List leads (CRM-lite)
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/LeadStatus"
      responses:
        "200":
          description: Leads
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [leads]
                    properties:
                      leads:
                        type: array
                        items:
                          $ref: "#/components/schemas/LeadSummary"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/leads/{leadId}:
    get:
      tags: [Leads]
      summary: Lead detail
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: leadId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Lead detail
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [lead, lastCampaign]
                    properties:
                      lead:
                        $ref: "#/components/schemas/LeadDetail"
                      lastCampaign:
                        type: object
                        nullable: true
                        required: [campaignId, presetId, channels, status, createdAt, updatedAt]
                        properties:
                          campaignId:
                            type: string
                          presetId:
                            type: string
                          channels:
                            type: array
                            items:
                              type: string
                              enum: [sms, email]
                          status:
                            type: string
                            enum: [active, paused, stopped]
                          createdAt:
                            type: string
                            format: date-time
                          updatedAt:
                            type: string
                            format: date-time
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      tags: [Leads]
      summary: Update lead status
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: leadId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  $ref: "#/components/schemas/LeadStatus"
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/leads/{leadId}/notes:
    post:
      tags: [Leads]
      summary: Add a note to a lead
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: leadId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/leads/{leadId}/messages:
    get:
      tags: [Leads]
      summary: Conversation thread for a lead
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: leadId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Messages
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [messages]
                    properties:
                      messages:
                        type: array
                        items:
                          $ref: "#/components/schemas/Message"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/campaigns/{campaignId}/stop:
    post:
      tags: [Campaigns]
      summary: Stop/pause a campaign
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/exports/leads.csv:
    get:
      tags: [Reporting]
      summary: Export leads as CSV
      description: Subscription required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: CSV export
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/exports/campaigns.csv:
    get:
      tags: [Reporting]
      summary: Export campaigns as CSV
      description: Subscription required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: CSV export
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/exports/messages.csv:
    get:
      tags: [Reporting]
      summary: Export message log as CSV
      description: Subscription required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: CSV export
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/exports/audit.csv:
    get:
      tags: [Reporting]
      summary: Export audit log as CSV
      description: Subscription required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: CSV export
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/reports/summary:
    get:
      tags: [Reporting]
      summary: Reporting summary metrics
      description: Subscription required.
      security:
        - bearerAuth: []
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Summary
          content:
            application/json:
              schema:
                type: object
                required: [ok, data]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [from, to, campaignsLaunched, repliesReceived, optOuts, leadsByStatus]
                    properties:
                      from:
                        type: string
                        format: date
                        nullable: true
                      to:
                        type: string
                        format: date
                        nullable: true
                      campaignsLaunched:
                        type: integer
                      repliesReceived:
                        type: integer
                      optOuts:
                        type: integer
                      leadsByStatus:
                        type: object
                        required: [new, warm, hot, closed, do_not_contact]
                        properties:
                          new:
                            type: integer
                          warm:
                            type: integer
                          hot:
                            type: integer
                          closed:
                            type: integer
                          do_not_contact:
                            type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
