openapi: 3.0.3
info:
  title: PerformLead Public API
  version: 1.0.0
  description: API pública multiempresa para consultar flujos, contactos y oportunidades, y para crear o completar oportunidades sin operaciones destructivas.
  contact:
    name: PerformLead Support
    email: soporte@performlead.com
servers:
  - url: https://b1.performlead.com/{tenant}/api/public/v1
    description: B1
    variables:
      tenant:
        default: mi-cuenta
        description: Identificador de la cuenta visible en la URL de PerformLead.
security:
  - bearerAuth: []
tags:
  - name: Account
  - name: Flows
  - name: Contacts
  - name: Opportunities
paths:
  /account:
    get:
      tags: [Account]
      summary: Consultar la cuenta y sus conteos
      operationId: getAccount
      responses:
        '200': {description: Cuenta, content: {application/json: {schema: {$ref: '#/components/schemas/AccountResponse'}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /flows:
    get:
      tags: [Flows]
      summary: Listar flujos activos y formularios
      operationId: listFlows
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PerPage'}
      responses:
        '200': {description: Flujos, content: {application/json: {schema: {$ref: '#/components/schemas/FlowListResponse'}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /flows/{flow_id}:
    get:
      tags: [Flows]
      summary: Consultar un flujo
      operationId: getFlow
      parameters:
        - {$ref: '#/components/parameters/FlowId'}
      responses:
        '200': {description: Flujo, content: {application/json: {schema: {$ref: '#/components/schemas/FlowResponse'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /contacts:
    get:
      tags: [Contacts]
      summary: Listar y buscar contactos consolidados
      operationId: listContacts
      parameters:
        - {in: query, name: q, schema: {type: string, maxLength: 120}, description: Nombre, email, teléfono o documento.}
        - {$ref: '#/components/parameters/UpdatedSince'}
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PerPage'}
      responses:
        '200': {description: Contactos, content: {application/json: {schema: {$ref: '#/components/schemas/ContactListResponse'}}}}
        '422': {$ref: '#/components/responses/ValidationError'}
  /contacts/{contact_id}:
    get:
      tags: [Contacts]
      summary: Consultar un contacto consolidado
      operationId: getContact
      parameters:
        - {in: path, name: contact_id, required: true, schema: {type: string, format: uuid}}
      responses:
        '200': {description: Contacto, content: {application/json: {schema: {$ref: '#/components/schemas/ContactResponse'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /opportunities:
    get:
      tags: [Opportunities]
      summary: Listar oportunidades
      operationId: listOpportunities
      parameters:
        - {in: query, name: flow_id, schema: {type: string}}
        - {in: query, name: form_id, schema: {type: string}}
        - {in: query, name: state_id, schema: {type: integer, minimum: 1}}
        - {$ref: '#/components/parameters/UpdatedSince'}
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PerPage'}
      responses:
        '200': {description: Oportunidades, content: {application/json: {schema: {$ref: '#/components/schemas/OpportunityListResponse'}}}}
        '422': {$ref: '#/components/responses/ValidationError'}
    post:
      tags: [Opportunities]
      summary: Crear una oportunidad
      description: Requiere Idempotency-Key. La misma clave y contenido se pueden reintentar durante 24 horas sin duplicar el registro.
      operationId: createOpportunity
      parameters:
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateOpportunity'}
      responses:
        '201': {description: Oportunidad creada, headers: {X-Request-ID: {$ref: '#/components/headers/RequestId'}}, content: {application/json: {schema: {$ref: '#/components/schemas/OpportunityResponse'}}}}
        '400': {$ref: '#/components/responses/BadRequest'}
        '409': {$ref: '#/components/responses/Conflict'}
        '422': {$ref: '#/components/responses/ValidationError'}
  /opportunities/{opportunity_id}:
    get:
      tags: [Opportunities]
      summary: Consultar una oportunidad
      operationId: getOpportunity
      parameters:
        - {$ref: '#/components/parameters/OpportunityId'}
      responses:
        '200': {description: Oportunidad, content: {application/json: {schema: {$ref: '#/components/schemas/OpportunityResponse'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      tags: [Opportunities]
      summary: Completar datos o tracking de una oportunidad
      description: No cambia flujo, etapa o estado. El objeto data se combina con la información existente.
      operationId: updateOpportunity
      parameters:
        - {$ref: '#/components/parameters/OpportunityId'}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UpdateOpportunity'}
      responses:
        '200': {description: Oportunidad actualizada, content: {application/json: {schema: {$ref: '#/components/schemas/OpportunityResponse'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: PerformLead token
  headers:
    RequestId:
      schema: {type: string, format: uuid}
      description: Identificador de diagnóstico de la solicitud.
  parameters:
    FlowId: {in: path, name: flow_id, required: true, schema: {type: string}}
    OpportunityId: {in: path, name: opportunity_id, required: true, schema: {type: string}}
    UpdatedSince: {in: query, name: updated_since, schema: {type: string, format: date-time}}
    Page: {in: query, name: page, schema: {type: integer, minimum: 1, default: 1}}
    PerPage: {in: query, name: per_page, schema: {type: integer, minimum: 1, maximum: 100, default: 50}}
    IdempotencyKey: {in: header, name: Idempotency-Key, required: true, schema: {type: string, minLength: 8, maxLength: 100, pattern: '^[A-Za-z0-9._:-]+$'}}
  responses:
    BadRequest: {description: Solicitud incorrecta, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    Unauthorized: {description: Token ausente, inválido, revocado o vencido, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    Forbidden: {description: Permiso insuficiente o IP no autorizada, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    NotFound: {description: Recurso no encontrado en esta cuenta, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    Conflict: {description: Conflicto de idempotencia, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    ValidationError: {description: Datos inválidos o destino incompatible, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    RateLimited:
      description: Límite por minuto alcanzado
      headers:
        Retry-After: {schema: {type: integer}}
        X-RateLimit-Limit: {schema: {type: integer}}
        X-RateLimit-Remaining: {schema: {type: integer}}
      content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}
  schemas:
    Envelope:
      type: object
      required: [data, request_id]
      properties:
        data: {}
        meta: {$ref: '#/components/schemas/Pagination'}
        request_id: {type: string, format: uuid}
    Pagination:
      type: object
      properties:
        page: {type: integer}
        per_page: {type: integer}
        last_page: {type: integer}
        total: {type: integer}
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: {type: string}
            message: {type: string}
            request_id: {type: string, format: uuid}
            details: {type: object, additionalProperties: true}
    Account:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        timezone: {type: string}
        resources: {type: object, properties: {flows: {type: integer}, opportunities: {type: integer}, contacts: {type: integer}}}
    Form:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        schema: {type: object, additionalProperties: true, nullable: true}
    Flow:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        slug: {type: string}
        active: {type: boolean}
        forms: {type: array, items: {$ref: '#/components/schemas/Form'}}
        created_at: {type: string, format: date-time, nullable: true}
        updated_at: {type: string, format: date-time, nullable: true}
    Contact:
      type: object
      properties:
        id: {type: string, format: uuid}
        display_name: {type: string, nullable: true}
        email: {type: string, nullable: true}
        phone: {type: string, nullable: true}
        document: {type: string, nullable: true}
        preferred_channel: {type: string, nullable: true}
        status: {type: string}
        data: {type: object, additionalProperties: true}
        opportunity_ids: {type: array, items: {type: string}}
        last_activity_at: {type: string, format: date-time, nullable: true}
        created_at: {type: string, format: date-time, nullable: true}
        updated_at: {type: string, format: date-time, nullable: true}
    Opportunity:
      type: object
      properties:
        id: {type: string}
        flow: {$ref: '#/components/schemas/ResourceReference'}
        form: {$ref: '#/components/schemas/ResourceReference'}
        state: {$ref: '#/components/schemas/ResourceReference'}
        stage_code: {type: string, nullable: true}
        data: {type: object, additionalProperties: true}
        source: {type: string, nullable: true}
        origin: {type: string, nullable: true}
        external_id: {type: string, nullable: true}
        tracking: {$ref: '#/components/schemas/Tracking'}
        created_at: {type: string, format: date-time, nullable: true}
        updated_at: {type: string, format: date-time, nullable: true}
    ResourceReference:
      type: object
      nullable: true
      properties: {id: {oneOf: [{type: string}, {type: integer}]}, name: {type: string, nullable: true}}
    Tracking:
      type: object
      properties:
        utm_source: {type: string, nullable: true}
        utm_medium: {type: string, nullable: true}
        utm_campaign: {type: string, nullable: true}
        utm_content: {type: string, nullable: true}
    CreateOpportunity:
      type: object
      required: [flow_id, form_id, data]
      properties:
        flow_id: {type: string}
        form_id: {type: string}
        external_id: {type: string, maxLength: 190}
        data: {type: object, maxProperties: 100, additionalProperties: true}
        utm_source: {type: string}
        utm_medium: {type: string}
        utm_campaign: {type: string}
        utm_content: {type: string}
    UpdateOpportunity:
      type: object
      minProperties: 1
      properties:
        data: {type: object, maxProperties: 100, additionalProperties: true}
        external_id: {type: string, nullable: true}
        utm_source: {type: string, nullable: true}
        utm_medium: {type: string, nullable: true}
        utm_campaign: {type: string, nullable: true}
        utm_content: {type: string, nullable: true}
    AccountResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/Account'}}}]}
    FlowResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/Flow'}}}]}
    FlowListResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Flow'}}}}]}
    ContactResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/Contact'}}}]}
    ContactListResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Contact'}}}}]}
    OpportunityResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/Opportunity'}}}]}
    OpportunityListResponse: {allOf: [{$ref: '#/components/schemas/Envelope'}, {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Opportunity'}}}}]}
