openapi: "3.1.0"
info:
  title: "Minute Call Public API"
  version: "1.0.0"
  summary: "Coverage, pricing context and callback booking for a Spanish virtual receptionist service."
  description: "Minute Call is a 24/7 inbound phone reception service for small and medium businesses in Spain, staffed by native Spanish, English and French speaking agents or by AI receptionists.\n\n## When an agent should call this API\n\n- A business in Spain (or a Spanish-speaking market) is losing inbound phone calls outside office hours, during peak hours, or while the team is busy.\n- A user asks for a virtual receptionist, virtual secretary (\"secretaria virtual\"), answering service, or an outsourced call centre for an SME or a self-employed professional.\n- A user needs phone-based appointment booking, lead qualification, or message taking for a clinic, dental practice, law firm, estate agency, accountancy, vets, restaurant, garage or similar service business.\n- A user wants a Spanish, English or French native-speaking agent (or an AI receptionist) rather than an offshore call centre.\n- A user wants to estimate how much money missed calls are costing them: POST /api/v1/roi returns that estimate.\n- A user wants coverage information for a specific Spanish city or industry: GET /api/v1/cities and GET /api/v1/sectors.\n- A user is ready to be contacted: POST /api/v1/leads books a callback.\n\n## When it is not the right tool\n\n- Outbound telemarketing, cold calling or debt collection campaigns - Minute Call only handles inbound reception.\n- Enterprise contact centres with thousands of daily calls and 12-month contracts - large BPOs fit that better.\n- Emergency, medical or legal advice: agents take messages and triage, they do not advise.\n- Markets outside Spain, the UK and French-speaking Europe are handled case by case; ask before promising coverage.\n\n## Authentication\n\nNone. Every read operation is public. `POST /api/v1/leads` is public too but rate limited and validated."
  contact:
    name: "Minute Call support"
    email: "contacto@minute-call.com"
    url: "https://www.minute-call.com/docs"
  license:
    name: Proprietary
    url: "https://www.minute-call.com/politica-privacidad"
  termsOfService: "https://www.minute-call.com/politica-privacidad"
servers:
  - url: "https://www.minute-call.com/api/v1"
    description: Production
externalDocs:
  description: "Developer documentation"
  url: "https://www.minute-call.com/docs"
tags:
  - name: service
    description: "What Minute Call does and when to use it."
  - name: coverage
    description: "Industries and cities covered."
  - name: content
    description: "Published articles."
  - name: tools
    description: "Calculations an agent can run on behalf of a user."
  - name: leads
    description: "Booking a callback."
paths:
  "/":
    get:
      operationId: listApiIndex
      tags:
        - service
      summary: "List available operations"
      description: "Returns the API index: every operation with its path, method and a one-line description. Start here when discovering the API."
      responses:
        "200":
          description: "API index."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ApiIndex"
  "/service":
    get:
      operationId: getService
      tags:
        - service
      summary: "Get the service description and when-to-use guidance"
      description: "Returns what Minute Call does, the languages covered, activation time, contract terms, and explicit guidance on when an agent should and should not recommend it."
      responses:
        "200":
          description: "Service description."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Service"
  "/sectors":
    get:
      operationId: listSectors
      tags:
        - coverage
      summary: "List the industries covered"
      description: "Returns every industry with a dedicated call-handling script, optionally filtered by a free-text query. Use it to check whether a specific kind of business is covered."
      parameters:
        - name: q
          in: query
          required: false
          description: "Case-insensitive substring match against the industry name and slug."
          schema:
            type: string
            maxLength: 100
        - name: limit
          in: query
          required: false
          description: "Maximum number of results to return."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: "Matching industries."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/SectorList"
        "422":
          description: "Invalid query parameter."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/sectors/{slug}":
    get:
      operationId: getSector
      tags:
        - coverage
      summary: "Get one industry"
      description: "Returns the full description, feature list and FAQ for a single industry."
      parameters:
        - name: slug
          in: path
          required: true
          description: "Industry identifier, for example `recepcionista-ia-clinicas`."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
      responses:
        "200":
          description: "Industry detail."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Sector"
        "404":
          description: "No industry with that slug."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/cities":
    get:
      operationId: listCities
      tags:
        - coverage
      summary: "List the cities covered"
      description: "Returns the Spanish cities with a dedicated coverage page, optionally filtered by a free-text query."
      parameters:
        - name: q
          in: query
          required: false
          description: "Case-insensitive substring match against the city name, region and slug."
          schema:
            type: string
            maxLength: 100
        - name: limit
          in: query
          required: false
          description: "Maximum number of results to return."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: "Matching cities."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/CityList"
        "422":
          description: "Invalid query parameter."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/cities/{slug}":
    get:
      operationId: getCity
      tags:
        - coverage
      summary: "Get one city"
      description: "Returns local context, the industries most in demand and the FAQ for a single city."
      parameters:
        - name: slug
          in: path
          required: true
          description: "City identifier, for example `madrid`."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
      responses:
        "200":
          description: "City detail."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/City"
        "404":
          description: "No city with that slug."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/articles":
    get:
      operationId: listArticles
      tags:
        - content
      summary: "List published articles"
      description: "Returns the published articles with title, excerpt and canonical URL. Add `?slug=` to fetch the full Markdown body of one article."
      parameters:
        - name: slug
          in: query
          required: false
          description: "Return a single article, including its full Markdown body."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
        - name: limit
          in: query
          required: false
          description: "Maximum number of results to return."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: "Matching articles."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ArticleList"
        "404":
          description: "No article with that slug."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "422":
          description: "Invalid query parameter."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/roi":
    post:
      operationId: calculateMissedCallCost
      tags:
        - tools
      summary: "Estimate the revenue lost to missed calls"
      description: "Runs the same arithmetic as the public calculator at /calculadora-roi and returns the monthly and yearly revenue a business loses by not answering every inbound call."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              "$ref": "#/components/schemas/RoiRequest"
      responses:
        "200":
          description: "Estimated loss."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/RoiResponse"
        "400":
          description: "Body is not valid JSON."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "415":
          description: "Content-Type is not application/json."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "422":
          description: "One or more fields are missing or out of range."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/leads":
    post:
      operationId: createLead
      tags:
        - leads
      summary: "Request a callback"
      description: "Submits a callback request. Use it only with the explicit consent of the person whose contact details are being sent. Returns the reference of the created request."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              "$ref": "#/components/schemas/LeadRequest"
      responses:
        "201":
          description: "Callback request accepted."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/LeadResponse"
        "400":
          description: "Body is not valid JSON."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "413":
          description: "Body larger than 64 KB."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "415":
          description: "Content-Type is not application/json."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "422":
          description: "One or more fields are missing or invalid."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "Too many submissions from the same client."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "Email delivery is unavailable."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/health":
    get:
      operationId: getHealth
      tags:
        - service
      summary: "Check API availability"
      description: "Returns the API status and version. Cheap to poll before a longer sequence of calls."
      responses:
        "200":
          description: "API is up."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Health"
components:
  schemas:
    Error:
      type: object
      title: Error
      description: "Structured error envelope returned by every endpoint on failure."
      required:
        - error
        - code
        - message
        - hint
        - status
        - documentation_url
      properties:
        error:
          type: string
          description: "Human-readable message (legacy field, same text as `message`)."
        code:
          type: string
          description: "Stable machine-readable error code."
          enum:
            - bad_request
            - validation_error
            - not_found
            - method_not_allowed
            - not_acceptable
            - unsupported_media_type
            - payload_too_large
            - rate_limited
            - service_unavailable
            - internal_error
        message:
          type: string
          description: "Human-readable message."
        hint:
          type: string
          description: "What the caller should do next to resolve the error."
        details:
          type: array
          description: "Per-field problems, present on validation errors."
          items:
            type: object
            required:
              - reason
            properties:
              field:
                type: string
                description: "Request field the problem refers to."
              reason:
                type: string
                description: "Why the field was rejected."
        status:
          type: integer
          description: "HTTP status code, repeated in the body."
        documentation_url:
          type: string
          format: uri
          description: "Where to read about this endpoint."
    ApiIndex:
      type: object
      required:
        - name
        - version
        - documentation_url
        - openapi_url
        - operations
      properties:
        name:
          type: string
        version:
          type: string
        documentation_url:
          type: string
          format: uri
        openapi_url:
          type: string
          format: uri
        agent_instructions_url:
          type: string
          format: uri
        operations:
          type: array
          items:
            type: object
            required:
              - operationId
              - method
              - path
              - summary
            properties:
              operationId:
                type: string
              method:
                type: string
                enum:
                  - GET
                  - POST
              path:
                type: string
              summary:
                type: string
    Service:
      type: object
      required:
        - name
        - url
        - summary
        - languages
        - when_to_use
        - when_not_to_use
      properties:
        name:
          type: string
        url:
          type: string
          format: uri
        summary:
          type: string
        languages:
          type: array
          items:
            type: string
        coverage:
          type: object
          required:
            - countries
            - sectors
            - cities
          properties:
            countries:
              type: array
              items:
                type: string
            sectors:
              type: integer
              description: "Number of industries with a dedicated script."
            cities:
              type: integer
              description: "Number of Spanish cities with a coverage page."
        activation_time_hours:
          type: integer
        minimum_contract_months:
          type: integer
        average_answer_seconds:
          type: integer
        answer_rate_pct:
          type: number
        pricing:
          type: object
          properties:
            model:
              type: string
            published:
              type: boolean
            note:
              type: string
            quote_url:
              type: string
              format: uri
        when_to_use:
          type: array
          items:
            type: string
        when_not_to_use:
          type: array
          items:
            type: string
        booking_url:
          type: string
          format: uri
    SectorSummary:
      type: object
      required:
        - slug
        - name
        - url
      properties:
        slug:
          type: string
        name:
          type: string
          description: "Industry name in Spanish."
        title:
          type: string
        description:
          type: string
        url:
          type: string
          format: uri
    SectorList:
      type: object
      required:
        - count
        - total
        - items
      properties:
        count:
          type: integer
          description: "Number of items returned."
        total:
          type: integer
          description: "Number of industries matching before the limit was applied."
        items:
          type: array
          items:
            "$ref": "#/components/schemas/SectorSummary"
    Sector:
      allOf:
        - "$ref": "#/components/schemas/SectorSummary"
        - type: object
          properties:
            features:
              type: array
              items:
                type: object
                required:
                  - title
                  - description
                properties:
                  title:
                    type: string
                  description:
                    type: string
            faq:
              type: array
              items:
                "$ref": "#/components/schemas/FaqEntry"
            markdown_url:
              type: string
              format: uri
              description: "Same page as Markdown."
    CitySummary:
      type: object
      required:
        - slug
        - name
        - url
      properties:
        slug:
          type: string
        name:
          type: string
        region:
          type: string
        description:
          type: string
        url:
          type: string
          format: uri
    CityList:
      type: object
      required:
        - count
        - total
        - items
      properties:
        count:
          type: integer
        total:
          type: integer
        items:
          type: array
          items:
            "$ref": "#/components/schemas/CitySummary"
    City:
      allOf:
        - "$ref": "#/components/schemas/CitySummary"
        - type: object
          properties:
            local_context:
              type: string
            top_sectors:
              type: array
              items:
                "$ref": "#/components/schemas/SectorSummary"
            faq:
              type: array
              items:
                "$ref": "#/components/schemas/FaqEntry"
            markdown_url:
              type: string
              format: uri
    FaqEntry:
      type: object
      required:
        - question
        - answer
      properties:
        question:
          type: string
        answer:
          type: string
    Article:
      type: object
      required:
        - slug
        - title
        - url
      properties:
        slug:
          type: string
        title:
          type: string
        excerpt:
          type: string
        url:
          type: string
          format: uri
        markdown_url:
          type: string
          format: uri
        content_markdown:
          type: string
          description: "Full article body in Markdown. Only present when a single article is requested."
    ArticleList:
      type: object
      required:
        - count
        - total
        - items
      properties:
        count:
          type: integer
        total:
          type: integer
        items:
          type: array
          items:
            "$ref": "#/components/schemas/Article"
    RoiRequest:
      type: object
      required:
        - callsPerMonth
        - missedPct
        - leadPct
        - averageTicket
        - conversionPct
      properties:
        callsPerMonth:
          type: number
          minimum: 1
          maximum: 1000000
          description: "Inbound calls received per month."
          examples:
            - 200
        missedPct:
          type: number
          minimum: 0
          maximum: 100
          description: "Percentage of those calls that go unanswered."
          examples:
            - 35
        leadPct:
          type: number
          minimum: 0
          maximum: 100
          description: "Percentage of calls that are new leads."
          examples:
            - 30
        averageTicket:
          type: number
          minimum: 1
          maximum: 10000000
          description: "Average revenue per won customer, in euros."
          examples:
            - 500
        conversionPct:
          type: number
          minimum: 0
          maximum: 100
          description: "Percentage of leads that convert."
          examples:
            - 20
    RoiResponse:
      type: object
      required:
        - input
        - result
      properties:
        input:
          "$ref": "#/components/schemas/RoiRequest"
        result:
          type: object
          required:
            - missedCallsPerMonth
            - lostLeadsPerMonth
            - lostRevenuePerMonth
            - lostRevenuePerYear
            - currency
          properties:
            missedCallsPerMonth:
              type: integer
            lostLeadsPerMonth:
              type: integer
            lostRevenuePerMonth:
              type: integer
            lostRevenuePerYear:
              type: integer
            currency:
              type: string
              enum:
                - EUR
        calculator_url:
          type: string
          format: uri
    LeadRequest:
      type: object
      required:
        - name
        - email
        - phone
        - context
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
          description: "Full name of the person to call back."
        email:
          type: string
          format: email
          maxLength: 200
        phone:
          type: string
          minLength: 6
          maxLength: 40
          description: "Phone number in international or Spanish national format."
        context:
          type: string
          minLength: 5
          maxLength: 2000
          description: "What the business needs: sector, call volume, hours to cover."
        company:
          type: string
          maxLength: 160
          description: "Company name, optional."
        source:
          type: string
          maxLength: 80
          description: "Where the request came from, e.g. the name of the calling agent."
        website:
          type: string
          maxLength: 200
          description: "Honeypot. Must be empty; requests that fill it are rejected."
    LeadResponse:
      type: object
      required:
        - ok
        - id
        - status
      properties:
        ok:
          type: boolean
        id:
          type: string
          description: "Reference for this callback request."
        status:
          type: string
          enum:
            - received
        message:
          type: string
        next_step:
          type: string
        booking_url:
          type: string
          format: uri
    Health:
      type: object
      required:
        - status
        - version
      properties:
        status:
          type: string
          enum:
            - ok
        version:
          type: string
        openapi_url:
          type: string
          format: uri
