openapi: 3.1.0
info:
  title: Airlead Partner API
  version: 0.1.0
  description: |
    Public partner-facing HTTP endpoints for Airlead (ingest + buyer lifecycle webhooks).
    Endpoints used internally by the Airlead web app are out of scope.
    Base host: https://app.airlead.de
  contact:
    name: Airlead
    url: https://app.airlead.de
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://app.airlead.de
    description: Production
security:
  - WebhookSecretHeader: []
  - WebhookSecretBearer: []
tags:
  - name: Ingest
    description: Send people and product interests to an Airlead source.
  - name: Partner lifecycle
    description: Report commercial outcomes for delivered transactions.
  - name: Utility
    description: Check service availability.
paths:
  /api/health:
    get:
      tags: [Utility]
      summary: Health check
      operationId: getHealth
      security: []
      responses:
        "200":
          description: Service is up
        "429":
          description: Too many requests
        "503":
          description: Service is unavailable

  /api/webhooks/channels/{channelId}/sources/{sourceId}/leads:
    post:
      tags: [Ingest]
      summary: Ingest leads into a channel source
      operationId: ingestChannelSourceLeads
      parameters:
        - $ref: "#/components/parameters/channelId"
        - $ref: "#/components/parameters/sourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChannelIngestBody"
            examples:
              flatLead:
                summary: Flat lead
                value:
                  externalId: crm-lead-1001
                  firstName: Alex
                  lastName: Example
                  email: alex@example.com
                  phone: "+491701234567"
                  postalCode: "10115"
                  city: Berlin
                  country: DE
                  consentStatus: granted
                  productSlug: heat-pump
                  routingReady: true
                  triggerAutoDelivery: false
                  fieldValues:
                    current_heating: oil
              batch:
                summary: Batch
                value:
                  leads:
                    - firstName: A
                      lastName: One
                    - firstName: B
                      lastName: Two
      responses:
        "201":
          description: Lead(s) created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/webhooks/lead-tables/{tableId}/sources/{sourceId}/leads:
    post:
      tags: [Ingest]
      summary: Ingest leads into a lead-table (list) source
      operationId: ingestLeadTableSourceLeads
      parameters:
        - $ref: "#/components/parameters/tableId"
        - $ref: "#/components/parameters/sourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TableIngestBody"
      responses:
        "201":
          description: Lead(s) created
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/webhooks/partner-confirmation:
    post:
      tags: [Partner lifecycle]
      summary: Acknowledge or reject a delivered transaction
      operationId: applyPartnerConfirmation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerConfirmationBody"
      responses:
        "200":
          description: Confirmation applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  item:
                    type: object
                    additionalProperties: true
                  autoStatusChangedTo:
                    type:
                      - string
                      - "null"
                  alreadyApplied:
                    type: boolean
                  economicsApplied:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Ambiguous partner external id — pass buyerAccountId

  /api/webhooks/partner-payout-pending:
    post:
      tags: [Partner lifecycle]
      summary: Mark transaction payout pending
      operationId: markPartnerPayoutPending
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransactionIdBody"
      responses:
        "200":
          description: Status updated
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/webhooks/partner-payout:
    post:
      tags: [Partner lifecycle]
      summary: Complete transaction payout
      operationId: completePartnerPayout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransactionIdBody"
      responses:
        "200":
          description: Payout completed
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/webhooks/partner-cancellation:
    post:
      tags: [Partner lifecycle]
      summary: Cancel a delivered transaction
      operationId: applyPartnerCancellation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerCancellationBody"
      responses:
        "200":
          description: Cancellation applied
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/webhooks/supplier-cancellation-confirmation:
    post:
      tags: [Partner lifecycle]
      summary: Confirm or reject supplier-side lead cancellation
      operationId: applySupplierCancellationConfirmation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SupplierCancellationBody"
      responses:
        "200":
          description: Cancellation confirmation recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:
                    type: boolean
                  code:
                    type: string
                  leadId:
                    type: string
                  alreadyApplied:
                    type: boolean
                  outcome:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

webhooks:
  leadDelivered:
    post:
      summary: Receive an Airlead buyer delivery
      description: Airlead sends this event to the webhook URL configured for a generic buyer integration.
      operationId: receiveLeadDelivered
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LeadDeliveredEvent"
      responses:
        "200":
          description: Delivery durably accepted by the buyer system
        "400":
          description: Payload rejected by the buyer system
        "500":
          description: Temporary buyer-system failure

components:
  securitySchemes:
    WebhookSecretHeader:
      type: apiKey
      in: header
      name: X-Airlead-Webhook-Secret
      description: Source ingest secret (if configured) or buyer webhook secret for lifecycle endpoints.
    WebhookSecretBearer:
      type: http
      scheme: bearer
      description: Same secret as X-Airlead-Webhook-Secret, sent as Bearer token.

  parameters:
    channelId:
      name: channelId
      in: path
      required: true
      schema:
        type: string
    sourceId:
      name: sourceId
      in: path
      required: true
      schema:
        type: string
    tableId:
      name: tableId
      in: path
      required: true
      schema:
        type: string

  responses:
    BadRequest:
      description: Invalid JSON or validation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    Unauthorized:
      description: Missing or invalid webhook secret
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    Forbidden:
      description: Buyer webhook secret not configured or capability denied
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"

  schemas:
    ErrorBody:
      type: object
      properties:
        error:
          description: Human-readable message or structured validation details
          oneOf:
            - type: string
            - type: object
              additionalProperties: true

    CaptureMeta:
      type: object
      additionalProperties: false
      properties:
        capturedByExternalId:
          type: string
        capturedByName:
          type: string
        capturedByTeam:
          type: string
        capturedAt:
          type: string
        captureMethod:
          type: string
        metadata:
          type: object
          additionalProperties: true

    InterestRequest:
      type: object
      additionalProperties: false
      properties:
        productSlug:
          type: string
        productId:
          type: string
        routingReady:
          type: boolean
        triggerAutoDelivery:
          type: boolean
        fieldValues:
          type: object
          additionalProperties:
            type: string

    Lead:
      type: object
      required: [firstName, lastName]
      properties:
        externalId:
          type: string
          minLength: 1
          maxLength: 128
        firstName:
          type: string
          minLength: 1
        lastName:
          type: string
          minLength: 1
        email:
          type: string
        phone:
          type: string
        street:
          type: string
        houseNumber:
          type: string
        postalCode:
          type: string
        city:
          type: string
        country:
          type: string
        consentStatus:
          type: string
        legalBasis:
          type: string
        consentCapturedAt:
          type: string
        consentTextVersion:
          type: string
        capturedByExternalId:
          type: string
        capturedByName:
          type: string
        capturedByTeam:
          type: string
        capturedAt:
          type: string
        captureMethod:
          type: string
        capture:
          $ref: "#/components/schemas/CaptureMeta"
        request:
          $ref: "#/components/schemas/InterestRequest"
        requests:
          type: array
          items:
            $ref: "#/components/schemas/InterestRequest"
        productSlug:
          type: string
        productId:
          type: string
        routingReady:
          type: boolean
        triggerAutoDelivery:
          type: boolean
        fieldValues:
          type: object
          additionalProperties:
            type: string

    ChannelIngestBody:
      oneOf:
        - $ref: "#/components/schemas/Lead"
        - type: object
          required: [person]
          properties:
            externalId:
              type: string
            person:
              $ref: "#/components/schemas/Lead"
            interests:
              type: array
              items:
                $ref: "#/components/schemas/InterestRequest"
            request:
              $ref: "#/components/schemas/InterestRequest"
            requests:
              type: array
              items:
                $ref: "#/components/schemas/InterestRequest"
            triggerAutoDelivery:
              type: boolean
        - type: object
          required: [leads]
          properties:
            leads:
              type: array
              minItems: 1
              maxItems: 100
              items:
                $ref: "#/components/schemas/Lead"

    TableIngestBody:
      oneOf:
        - $ref: "#/components/schemas/Lead"
        - type: object
          required: [leads]
          properties:
            leads:
              type: array
              minItems: 1
              maxItems: 100
              items:
                $ref: "#/components/schemas/Lead"

    PartnerConfirmationBody:
      type: object
      required: [status]
      properties:
        transactionId:
          type: string
        externalId:
          type: string
        partnerExternalId:
          type: string
        buyerAccountId:
          type: string
        status:
          type: string
          enum: [acknowledged, rejected]
        reason:
          type: string
        buyerPrice:
          type: number
      description: At least one of transactionId, externalId, or partnerExternalId is required.

    TransactionIdBody:
      type: object
      required: [transactionId]
      properties:
        transactionId:
          type: string
        reason:
          type: string

    PartnerCancellationBody:
      type: object
      required: [transactionId, reason]
      properties:
        transactionId:
          type: string
        reason:
          type: string
          minLength: 1

    SupplierCancellationBody:
      type: object
      required: [leadId, status]
      properties:
        leadId:
          type: string
        status:
          type: string
          enum: [acknowledged, rejected]
        reason:
          type: string

    LeadDeliveredEvent:
      type: object
      description: Outbound payload Airlead POSTs to generic buyer webhooks (not an inbound API).
      properties:
        event:
          type: string
          const: lead.delivered
        transactionId:
          type: string
        deliveryChannel:
          type: string
          const: webhook
        leadRequestId:
          type: string
        leadId:
          type: string
        deliveryMode:
          type: string
          enum: [exclusive, multi]
        sentAt:
          type: string
          format: date-time
        buyer:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
        product:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            slug:
              type: string
        pricing:
          type: object
          properties:
            buyerPrice:
              type: number
            supplierCost:
              type: number
            marginAmount:
              type: number
        lead:
          type: object
          additionalProperties: true
        fieldValues:
          type: object
          additionalProperties:
            type:
              - string
              - "null"
