OpenMercantil Support API

Customer-support writes. Anonymous creation requires explicit privacy consent; replies require an authenticated owner session and CSRF. Ticket data is never exposed through the public MCP.

OpenAPI Specification

openmercantil-support-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: OpenMercantil Support API
  version: 1.9.3
  summary: Versioned public-read, browser-account, billing, support and provider-callback contracts.
  description: 'Public JSON API for Spanish company information derived from BORME and other public sources.
    OpenMercantil is an independent informational service; it is NOT the BOE, BORME or Registro Mercantil
    and does NOT replace official certificates or registry extracts.


    **Rate limits.** Free: 60 req/min y 200 req/día por IP. Planes superiores (Profesional 5.000 req/día,
    MAX 50.000 req/día, Enterprise 500.000+ req/día) según cuenta y API key. Cabeceras `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-OpenMercantil-Plan`, `Retry-After`.


    **License and attribution.** Source-specific metadata in each response and the active versioned source
    catalog prevails. OpenMercantil does not relicense upstream content under a blanket license. Unknown,
    review and restricted datasets are omitted or return `503 legal_layer_unavailable`. BOE/BORME material
    is re-used under Ley 37/2007 and its official version remains boe.es. Court judgments are not exposed;
    CENDOJ remains citation-index only under CGPJ Reglamento 3/2010.


    **Machine-readable catalog (DCAT-AP-ES):** https://openmercantil.es/catalog.rdf'
  termsOfService: https://openmercantil.es/terminos-de-uso
  contact:
    name: OpenMercantil
    url: https://openmercantil.es/soporte
    email: social@openmercantil.es
  license:
    name: Source-specific upstream terms; see response catalog metadata
    url: https://openmercantil.es/terminos-de-uso
  x-publisher:
    name: OpenMercantil
    url: https://openmercantil.es/
    email: social@openmercantil.es
  x-spatial: http://publications.europa.eu/resource/authority/country/ESP
  x-temporal: 2009-01-01/..
  x-language: es
  x-dcat-catalog: https://openmercantil.es/catalog.rdf
  x-rate-limit:
    free:
      per_min: 60
      per_day: 200
      kind: anonymous-ip
    profesional:
      per_min: 120
      per_day: 5000
      kind: api-key
    max:
      per_min: 600
      per_day: 50000
      kind: api-key
    enterprise:
      per_min: 1200
      per_day: 500000
      kind: contract
  x-methodology: https://openmercantil.es/metodologia
  x-sources: https://openmercantil.es/fuentes
  x-corrections: https://openmercantil.es/correcciones
  x-contract-status: Public read, browser-account and provider-callback surfaces are explicitly separated
    in this contract. Operator/admin routes are excluded. The public MCP consumes only the allowlisted
    GET read plane.
  x-account-segment-contract:
    projection: company_public_v2 immutable corporate sidecar
    synchronous_row_cap: 500
    bounded_count_cap: 50001
    count_semantics: The segment run response count is the number of rows returned, never a global total.
      Dataset preview uses total_is_lower_bound=true and total_lower_bound when the bounded count reaches
      50001.
    related_web_dataset_surface:
      preview_path: /mi-cuenta/datasets/preview
      export_path: /mi-cuenta/datasets/export.csv
      synchronous_export_max_rows: 500
      overflow_status: 503
      overflow_error: async_export_required
  x-company-identity-contract:
    version: '1.0'
    projection: company_public_v2 immutable generation-bound corporate sidecar
    applies_to: Every /api/v1/company/{slug}*, /api/v1/empresa/{slug}* and /api/v1/grafo/{slug} read before
      any report, cache, graph or dataset lookup. /api/v1/companies/compare resolves both requested subjects
      in one bounded company_public_v2 batch before either row is exposed; MCP company tools inherit these
      preflights through REST.
    resolution:
      published: canonical corporate slug admitted
      safe_alias: internally canonicalized and Content-Location emitted
      withheld: neutral 404; includes absent, personal and ambiguous/quarantined identities
      unavailable: 503 with no-store; clients must not infer absence
    search: Exact corporate CIF, exact canonical/safe-alias slug, or bounded name_prefix2 pool scored
      in application code. DNI/NIE and ambiguous CIFs return zero items.
    public_company_count: company_public_projection_state.row_count
servers:
- url: https://openmercantil.es
  description: Production
tags:
- name: Support
  description: Customer-support writes. Anonymous creation requires explicit privacy consent; replies
    require an authenticated owner session and CSRF. Ticket data is never exposed through the public MCP.
paths:
  /api/v1/support/ticket:
    post:
      operationId: createSupportTicket
      tags:
      - Support
      summary: Create a customer-support ticket
      description: Public write exception for customer support. Authentication is optional, but the boolean
        `accept_privacy=true` is mandatory and is persisted as the purpose-bound audit event `privacy_accepted`
        / `privacy_policy_v1`. The submitted contact details, message, client IP and a user-agent capped
        by the runtime are stored in the account database solely to process, audit and protect the support
        request; ticket contents are never public or available to the public MCP. Related context accepts
        only a validated company slug and a local or canonical-origin OpenMercantil URL, which is stored
        as text and never dereferenced. The JSON body is capped at 32 KiB.
      x-plane: account-support
      x-max-body-bytes: 32768
      x-rate-limit: API plan policy plus an anti-abuse ceiling of 5 creations per client IP per hour
      x-mcp-excluded: true
      security:
      - {}
      - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SupportTicketCreateRequest'
      responses:
        '201':
          description: Ticket created; only its internal numeric id and non-secret public reference are
            returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportTicketCreatedResponse'
        '400':
          description: Invalid JSON fields, validation failure or missing strict privacy consent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportRequestErrorResponse'
        '413':
          description: Request body exceeds 32 KiB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestBodyTooLargeError'
              example:
                error: request_body_too_large
                max_bytes: 32768
        '429':
          description: API plan quota exhausted or the per-IP ceiling of 5 ticket creations per hour was
            reached
          headers:
            Retry-After:
              description: Seconds before retrying
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportRequestErrorResponse'
        '503':
          description: The account database or migrated support schema is unavailable; creation fails
            closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportErrorResponse'
              example:
                ok: false
                error: Servicio no disponible.
  /api/v1/support/ticket/{id}/reply:
    post:
      operationId: replySupportTicket
      tags:
      - Support
      - User
      summary: Reply to a support ticket owned by the authenticated user
      description: Requires a fresh account session, `X-CSRF-Token`, ownership of the numeric ticket id
        and a non-closed ticket. The reply is persisted before best-effort notification. The JSON body
        is capped at 32 KiB and ticket contents are never exposed through the public MCP.
      x-plane: account-support
      x-max-body-bytes: 32768
      x-rate-limit: API plan policy plus a per-user action budget of 20 replies per 15 minutes and 100
        per day
      x-mcp-excluded: true
      security:
      - cookieAuth: []
      parameters:
      - name: id
        in: path
        required: true
        description: Internal ticket id; ownership is revalidated server-side.
        schema:
          type: integer
          minimum: 1
      - $ref: '#/components/parameters/CsrfHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SupportReplyRequest'
      responses:
        '200':
          description: Reply persisted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
              example:
                ok: true
        '400':
          description: Reply validation failed or the ticket is closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportRequestErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token, ticket not found under this account, or caller is not the owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportErrorResponse'
        '413':
          description: Request body exceeds 32 KiB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestBodyTooLargeError'
              example:
                error: request_body_too_large
                max_bytes: 32768
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: The action-budget store, account database or migrated support schema is unavailable;
            reply fails closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportRequestErrorResponse'
              example:
                ok: false
                error: Servicio no disponible.
components:
  parameters:
    CsrfHeader:
      name: X-CSRF-Token
      in: header
      required: true
      description: Fresh token returned by GET /api/v1/user/me for this authenticated session.
      schema:
        type: string
        minLength: 32
        maxLength: 256
  responses:
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication required (no active session)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    ErrorResponse:
      type: object
      description: Closed compatibility envelope for public/account errors. Route-specific schemas narrow
        these fields further where required.
      required:
      - error
      properties:
        error:
          type: string
          minLength: 1
        message:
          type: string
        detail:
          type: string
        code:
          type: string
        status:
          type:
          - integer
          - string
        projection:
          type: string
        reason:
          type: string
        source_catalog_version:
          type: string
        allowed_parameters:
          type: array
          uniqueItems: true
          items:
            type: string
        slug:
          type: string
        key:
          type: string
        maximum:
          type: integer
          minimum: 1
        parameter:
          type: string
        fields:
          type: array
          items:
            type: string
        max_bytes:
          type: integer
          minimum: 1
        allowed:
          type: array
          items:
            $ref: '#/components/schemas/JsonValue'
        valid:
          type: array
          items:
            $ref: '#/components/schemas/JsonValue'
        date:
          type: string
        login_url:
          type: string
        plan:
          type: string
        limited_by:
          type: string
          enum:
          - minute
          - day
        daily_limit:
          type: integer
          minimum: 1
        reset_at:
          type: integer
          minimum: 1
        reset_at_human:
          type: string
          format: date-time
        retry_after_s:
          type: integer
          minimum: 1
        retry_after:
          type: integer
          minimum: 1
        upgrade:
          type: string
          format: uri
        upgrade_url:
          type: string
        action:
          type: string
        limit:
          type: integer
          minimum: 0
        remaining:
          type: integer
          minimum: 0
        needed:
          type: integer
          minimum: 0
        shortfall:
          type: integer
          minimum: 0
        ok:
          type: boolean
        _alias_of:
          type: string
      additionalProperties: false
    JsonValue:
      description: A JSON value used only inside explicitly documented extension maps.
      oneOf:
      - type:
        - string
        - number
        - boolean
        - 'null'
      - type: array
        items:
          $ref: '#/components/schemas/JsonValue'
      - type: object
        additionalProperties:
          $ref: '#/components/schemas/JsonValue'
    OkResponse:
      type: object
      required:
      - ok
      properties:
        ok:
          type: boolean
      additionalProperties: false
    RequestBodyTooLargeError:
      type: object
      required:
      - error
      - max_bytes
      properties:
        error:
          type: string
          const: request_body_too_large
        max_bytes:
          type: integer
          minimum: 1
      additionalProperties: false
    SupportErrorResponse:
      type: object
      required:
      - ok
      - error
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
        code:
          type: integer
          description: Present on selected fail-closed or anti-abuse helper failures.
        retry_after:
          type: integer
          minimum: 1
          description: Present when the per-IP support creation ceiling is exhausted.
      additionalProperties: false
    SupportReplyRequest:
      type: object
      required:
      - body
      properties:
        body:
          type: string
          minLength: 5
          maxLength: 10000
          description: Trimmed before the 5–10,000 character validation.
      additionalProperties: false
    SupportRequestErrorResponse:
      description: Support validation/ownership errors carry `ok=false`; shared body-reader and action-budget
        errors use the generic API error envelope.
      anyOf:
      - $ref: '#/components/schemas/SupportErrorResponse'
      - $ref: '#/components/schemas/ErrorResponse'
    SupportTicketCategory:
      type: string
      description: Stable support-routing category accepted by the current account schema.
      enum:
      - comercial
      - tecnico
      - datos
      - rgpd
      - partnerships
      - sugerencia
      - bug
      - otro
    SupportTicketCreateRequest:
      type: object
      description: Customer-supplied support content. Privacy consent is purpose-bound to handling and
        protecting this request; it is not marketing consent.
      required:
      - email
      - category
      - subject
      - body
      - accept_privacy
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        name:
          type: string
          maxLength: 200
          pattern: ^[^\u0000-\u001F\u007F]*$
          description: Trimmed before persistence; ASCII control characters are rejected.
        category:
          $ref: '#/components/schemas/SupportTicketCategory'
        subject:
          type: string
          minLength: 6
          maxLength: 200
          pattern: ^[^\u0000-\u001F\u007F]+$
          description: Trimmed before length validation; ASCII control characters are rejected.
        body:
          type: string
          minLength: 20
          maxLength: 10000
          pattern: ^[^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*$
          description: Trimmed before length validation. TAB, CR and LF are permitted; other ASCII control
            characters are rejected.
        related_company_slug:
          type: string
          maxLength: 200
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          description: Optional validated context; it does not establish identity or ownership.
        related_url:
          type: string
          maxLength: 2048
          description: Optional context restricted at runtime to a single-slash absolute same-origin path
            or an HTTPS URL on exactly openmercantil.es, without userinfo, port, backslash or control
            characters. Query and fragment are removed before persistence. The URL is stored as text and
            never dereferenced.
        accept_privacy:
          type: boolean
          const: true
          description: Strict purpose-bound acceptance, persisted as privacy_accepted/privacy_policy_v1;
            never interpreted as marketing consent.
      additionalProperties: false
    SupportTicketCreatedResponse:
      type: object
      required:
      - ok
      - ticket_id
      - public_id
      properties:
        ok:
          type: boolean
          const: true
        ticket_id:
          type: integer
          minimum: 1
        public_id:
          type: string
          pattern: ^OB-[0-9]{4}-[0-9]{6,}$
      additionalProperties: false
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: ob_sess
      description: Browser session cookie set after login at /mi-cuenta/login. Mutations also require
        X-CSRF-Token header (obtain via GET /api/v1/user/me).