openapi: 3.0.3
info:
  title: 2BASE Public API
  version: 1.0.0
  description: |
    Read-only access to the 2BASE directory of Russian companies.

    The API requires no key. Phone numbers and email addresses are masked in
    public responses. Rate limit: 30 requests per minute per IP. Pagination is
    limited to 100 records per page and 200 pages per filtered result set.
  termsOfService: https://2base.ru/terms
  contact:
    name: 2BASE
    url: https://2base.ru/api/docs
    email: info@2base.ru
  license:
    name: MIT
    url: https://github.com/Decotel/2base-api/blob/main/LICENSE
externalDocs:
  description: Live API documentation
  url: https://2base.ru/api/docs
servers:
  - url: https://2base.ru
    description: Production
# All documented operations are intentionally available without an API key.
security: []
tags:
  - name: Companies
    description: Company directory and search
  - name: Dictionaries
    description: Regions, cities, categories, and OKVED codes
  - name: Service
    description: Aggregate statistics and service health
paths:
  /api/v1/companies:
    get:
      operationId: listCompanies
      summary: List companies
      description: Returns a filtered, paginated company list with masked contacts.
      tags: [Companies]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: sort
          in: query
          schema:
            type: string
            enum: [id, name]
            default: id
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - name: region_id
          in: query
          description: Region identifier from `/api/v1/regions`.
          schema: {type: integer, minimum: 1}
        - name: city_id
          in: query
          description: City identifier from `/api/v1/cities`.
          schema: {type: integer, minimum: 1}
        - name: parent_category_id
          in: query
          schema: {type: integer, minimum: 1}
        - name: parent_category
          in: query
          description: Parent category name. Ignored when `parent_category_id` is present.
          schema: {type: string}
        - name: category
          in: query
          description: Business subcategory name.
          schema: {type: string}
        - name: okved
          in: query
          description: OKVED code or code prefix.
          schema: {type: string}
        - name: type
          in: query
          description: Legal form, for example `ЮЛ` or `ИП`.
          schema: {type: string}
        - name: has_phone
          in: query
          schema: {type: boolean}
        - name: has_email
          in: query
          schema: {type: boolean}
        - name: has_website
          in: query
          schema: {type: boolean}
        - name: has_messenger
          in: query
          schema: {type: boolean}
        - name: q
          in: query
          description: Text filter.
          schema: {type: string}
      responses:
        '200':
          description: Company page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyListResponse'
        '400': {$ref: '#/components/responses/BadRequest'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/companies/{slug}:
    get:
      operationId: getCompany
      summary: Get a company by slug
      tags: [Companies]
      parameters:
        - name: slug
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          description: Company record with masked contacts.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {$ref: '#/components/schemas/Company'}
        '404':
          description: Company not found.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/search:
    get:
      operationId: searchCompanies
      summary: Search companies
      description: Full-text company search with highlighted fields in `_formatted`.
      tags: [Companies]
      parameters:
        - name: q
          in: query
          required: true
          schema: {type: string, minLength: 1}
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPageSearch'
      responses:
        '200':
          description: Search results.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SearchResponse'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/regions:
    get:
      operationId: listRegions
      summary: List Russian regions
      tags: [Dictionaries]
      responses:
        '200':
          description: Region dictionary.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Region'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/cities:
    get:
      operationId: listOrSearchCities
      summary: List cities in a region or search all cities
      description: Supply either `region_id` or `q`. If both are supplied, `region_id` takes precedence.
      tags: [Dictionaries]
      parameters:
        - name: region_id
          in: query
          schema: {type: integer, minimum: 1}
        - name: q
          in: query
          schema: {type: string}
      responses:
        '200':
          description: City dictionary.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/City'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/subcategories:
    get:
      operationId: listOrSearchSubcategories
      summary: List subcategories or search by name
      description: Supply `parent_category_id` or a `q` value containing at least two characters.
      tags: [Dictionaries]
      parameters:
        - name: parent_category_id
          in: query
          schema: {type: integer, minimum: 1}
        - name: q
          in: query
          schema: {type: string, minLength: 2}
      responses:
        '200':
          description: Business subcategories.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/BusinessCategory'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/okveds:
    get:
      operationId: searchOkveds
      summary: Search OKVED codes
      tags: [Dictionaries]
      parameters:
        - name: q
          in: query
          required: true
          description: At least two characters from a dotted code or activity name.
          schema: {type: string, minLength: 2}
      responses:
        '200':
          description: Matching OKVED entries.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Okved'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/v1/stats:
    get:
      operationId: getStatistics
      summary: Get current directory statistics
      tags: [Service]
      responses:
        '200':
          description: Aggregate counts.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {$ref: '#/components/schemas/Stats'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/ServerError'}
  /api/health:
    get:
      operationId: getHealth
      summary: Check service health
      tags: [Service]
      responses:
        '200':
          description: Service is available.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
components:
  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    PerPageSearch:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
  responses:
    BadRequest:
      description: Invalid or missing query parameter.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
    RateLimited:
      description: Rate limit exceeded. Retry after the indicated interval.
      headers:
        Retry-After:
          schema: {type: integer}
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
    ServerError:
      description: Internal server error.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: {type: string}
    Pagination:
      type: object
      required: [page, per_page, total, total_pages]
      properties:
        page: {type: integer, minimum: 1}
        per_page: {type: integer, minimum: 1, maximum: 100}
        total: {type: integer, minimum: 0}
        total_pages: {type: integer, minimum: 0}
    CompanyListResponse:
      type: object
      required: [data, pagination]
      properties:
        data:
          type: array
          items: {$ref: '#/components/schemas/CompanySummary'}
        pagination: {$ref: '#/components/schemas/Pagination'}
    CompanySummary:
      type: object
      required: [id, name, slug, updated_at]
      additionalProperties: true
      properties:
        id: {type: integer, format: int64}
        inn: {type: string, nullable: true}
        ogrn: {type: string, nullable: true}
        name: {type: string}
        slug: {type: string}
        legal_form: {type: string, nullable: true}
        okved_name: {type: string, nullable: true}
        address: {type: string, nullable: true}
        city_name: {type: string, nullable: true}
        region_name: {type: string, nullable: true}
        phone:
          type: string
          nullable: true
          description: Masked public phone number.
          example: '+7 (999) ***-**-99'
        email:
          type: string
          nullable: true
          description: Masked public email address.
          example: 'in***@example.ru'
        website: {type: string, nullable: true}
        status: {type: string, nullable: true}
        employees: {type: integer, nullable: true}
        business_category: {type: string, nullable: true}
        parent_category: {type: string, nullable: true}
        updated_at: {type: string, format: date-time}
    Company:
      allOf:
        - $ref: '#/components/schemas/CompanySummary'
        - type: object
          additionalProperties: true
          properties:
            short_name: {type: string, nullable: true}
            description: {type: string, nullable: true}
            kpp: {type: string, nullable: true}
            okved_code: {type: string, nullable: true}
            region_id: {type: integer, nullable: true}
            city_id: {type: integer, nullable: true}
            district: {type: string, nullable: true}
            latitude: {type: number, format: double, nullable: true}
            longitude: {type: number, format: double, nullable: true}
            director: {type: string, nullable: true}
            director_position: {type: string, nullable: true}
            registration_date: {type: string, format: date-time, nullable: true}
            msp_category: {type: string, nullable: true}
            employees: {type: integer, nullable: true}
            rating: {type: number, format: double, nullable: true}
            reviews_count: {type: integer}
            schedule: {type: string, nullable: true}
            vk: {type: string, nullable: true}
            telegram: {type: string, nullable: true}
            whatsapp: {type: string, nullable: true}
            created_at: {type: string, format: date-time}
    SearchHit:
      type: object
      required: [id, name, slug]
      additionalProperties: true
      properties:
        id: {type: integer, format: int64}
        name: {type: string}
        slug: {type: string}
        inn: {type: string, nullable: true}
        short_name: {type: string, nullable: true}
        legal_form: {type: string, nullable: true}
        status: {type: string, nullable: true}
        okved_name: {type: string, nullable: true}
        business_category: {type: string, nullable: true}
        parent_category: {type: string, nullable: true}
        region: {type: string, nullable: true}
        city: {type: string, nullable: true}
        address: {type: string, nullable: true}
        phone: {type: string, nullable: true, description: Masked public phone number.}
        email: {type: string, nullable: true, description: Masked public email address.}
        rating: {type: number, format: double, nullable: true}
        reviews_count: {type: integer, nullable: true}
        _formatted:
          type: object
          description: Highlighted display values; matching fragments can contain `<em>` tags.
          additionalProperties: true
    SearchResponse:
      type: object
      required: [data, total, page]
      properties:
        data:
          type: array
          items: {$ref: '#/components/schemas/SearchHit'}
        total: {type: integer, minimum: 0}
        page: {type: integer, minimum: 1}
    Region:
      type: object
      required: [id, name, slug, code, company_count, active_company_count]
      properties:
        id: {type: integer}
        name: {type: string}
        slug: {type: string}
        code: {type: string}
        company_count: {type: integer, minimum: 0}
        active_company_count: {type: integer, minimum: 0}
    City:
      type: object
      required: [id, name, slug, region_id, company_count]
      properties:
        id: {type: integer}
        name: {type: string}
        slug: {type: string}
        region_id: {type: integer}
        company_count: {type: integer, minimum: 0}
        region_name: {type: string, nullable: true}
    BusinessCategory:
      type: object
      required: [id, name, slug, company_count]
      properties:
        id: {type: integer}
        name: {type: string}
        slug: {type: string}
        parent_category_id: {type: integer, nullable: true}
        parent_name: {type: string, nullable: true}
        company_count: {type: integer, minimum: 0}
    Okved:
      type: object
      required: [id, code, name, slug, company_count]
      properties:
        id: {type: integer}
        code: {type: string}
        name: {type: string}
        slug: {type: string}
        parent_code: {type: string, nullable: true}
        company_count: {type: integer, minimum: 0}
    Stats:
      type: object
      required: [categories, companies, phones, regions]
      properties:
        categories: {type: integer, minimum: 0}
        companies: {type: integer, minimum: 0}
        phones: {type: integer, minimum: 0}
        regions: {type: integer, minimum: 0}
