OpenMercantil Webhooks API

Account outbound webhooks: register, update, rotate the HMAC signing secret and delete event subscriptions. Three subscribable event types; deliveries are signed and fail closed on unknown events.

OpenAPI Specification

openmercantil-webhooks-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: OpenMercantil Webhooks 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: Webhooks
paths:
  /api/v1/user/webhooks:
    get:
      operationId: listUserWebhooks
      tags:
      - User
      - Webhooks
      summary: List outbound webhook metadata
      description: Account-plane metadata, the exact payload-contract 1.0 event allowlist and worker health.
        Full signing secrets are never listed; signing secrets are encrypted at rest with a versioned
        keyring. Unknown event types and schema versions fail closed.
      x-webhook-event-contract-version: '1.0'
      x-plane: account
      security:
      - cookieAuth: []
      responses:
        '200':
          description: Webhook metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/OutboundWebhook'
                  allowed_events:
                    $ref: '#/components/schemas/OutboundWebhookEventCatalogV1'
                  contracted_events:
                    $ref: '#/components/schemas/OutboundWebhookContractedEventCatalogV1'
                  worker:
                    $ref: '#/components/schemas/WebhookWorkerHealth'
                  secret_visible_once:
                    type: boolean
                    const: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Account or webhook security schema unavailable
    post:
      operationId: createUserWebhook
      tags:
      - User
      - Webhooks
      summary: Create an outbound webhook
      description: Validates HTTPS destination against SSRF, accepts only the explicit payload-contract
        1.0 event allowlist, and atomically stores both the encrypted signing secret and an encrypted
        24-hour idempotency replay. An identical Idempotency-Key retry returns the same secret without
        creating a second webhook. Wildcards and unknown events fail closed with 400.
      x-webhook-event-contract-version: '1.0'
      x-plane: account
      security:
      - cookieAuth: []
      parameters:
      - $ref: '#/components/parameters/CsrfHeader'
      - $ref: '#/components/parameters/AccountIdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OutboundWebhookCreateRequest'
      responses:
        '201':
          description: Created, or the exact signing secret replayed for the same key and payload
          headers:
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
            Idempotency-Key-Expires:
              $ref: '#/components/headers/IdempotencyKeyExpires'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundWebhookOneTimeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '409':
          description: Idempotency-Key payload conflict or expired replay window
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Webhook service, secret keyring, or durable idempotency schema/keyring unavailable
  /api/v1/user/webhooks/{id}:
    patch:
      operationId: updateUserWebhook
      tags:
      - User
      - Webhooks
      summary: Update an outbound webhook
      description: Revalidates URL/DNS/SSRF on every destination change and accepts only payload-contract
        1.0 event types. Wildcards and unknown events fail closed with 400. The secret is not returned.
      x-webhook-event-contract-version: '1.0'
      x-plane: account
      security:
      - cookieAuth: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
      - name: X-CSRF-Token
        in: header
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OutboundWebhookUpdateRequest'
      responses:
        '200':
          description: Updated metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookUpdatedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Account security or webhook schema unavailable
    delete:
      operationId: deleteUserWebhook
      tags:
      - User
      - Webhooks
      summary: Delete an outbound webhook
      x-plane: account
      security:
      - cookieAuth: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
      - name: X-CSRF-Token
        in: header
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Account security or webhook schema unavailable
  /api/v1/user/webhooks/{id}/rotate:
    post:
      operationId: rotateUserWebhookSecret
      tags:
      - User
      - Webhooks
      summary: Rotate an outbound webhook signing secret
      description: Cancels pending deliveries signed with the old key and atomically stores the encrypted
        replacement response. An identical Idempotency-Key retry within 24 hours returns the same secret
        without rotating again.
      x-plane: account
      security:
      - cookieAuth: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
      - $ref: '#/components/parameters/CsrfHeader'
      - $ref: '#/components/parameters/AccountIdempotencyKeyHeader'
      responses:
        '200':
          description: Rotated, or the exact replacement signing secret replayed for the same key
          headers:
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
            Idempotency-Key-Expires:
              $ref: '#/components/headers/IdempotencyKeyExpires'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundWebhookOneTimeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Idempotency-Key payload conflict or expired replay window
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Account security, encryption keyring or webhook schema unavailable
components:
  headers:
    IdempotencyKeyExpires:
      description: Unix timestamp after which this key cannot replay the one-time secret and a fresh key
        is required.
      schema:
        type: integer
        minimum: 1
    IdempotencyReplayed:
      description: True when this response was recovered from the durable encrypted replay record for
        the same key and payload.
      schema:
        type: boolean
    NoStoreCacheControl:
      description: Error responses must not be stored.
      schema:
        type: string
        const: no-store
  parameters:
    AccountIdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      description: Required for account mutations that reveal a secret. Reuse the same key only for an
        identical payload. The encrypted response can be replayed for 24 hours; a changed payload or an
        expired key returns 409.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: ^[A-Za-z0-9][A-Za-z0-9._:\-]{7,127}$
    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:
    BadRequest:
      description: Invalid request
      headers:
        Cache-Control:
          $ref: '#/components/headers/NoStoreCacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      headers:
        Cache-Control:
          $ref: '#/components/headers/NoStoreCacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    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:
    DeletedResponse:
      type: object
      required:
      - ok
      - deleted
      properties:
        ok:
          type: boolean
        deleted:
          type: boolean
      additionalProperties: false
    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'
    OutboundWebhook:
      type: object
      required:
      - id
      - url
      - events
      - active
      - secret_prefix
      - created_at
      properties:
        id:
          type: integer
          minimum: 1
        url:
          type: string
          format: uri
          pattern: ^https://
        events:
          $ref: '#/components/schemas/OutboundWebhookEventSubscriptionsV1'
        active:
          type: boolean
        secret_prefix:
          type: string
          maxLength: 12
          description: Non-secret identification prefix.
        key_id:
          type:
          - string
          - 'null'
          description: Public identifier of the current HMAC signing key.
        secret_storage:
          type: string
          enum:
          - encrypted
          - migration_required
        last_success_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
      additionalProperties: false
    OutboundWebhookContractedEventCatalogV1:
      type: array
      description: All event payload contracts known by this API version, including contracted-but-unwired
        events that cannot yet be subscribed.
      items:
        $ref: '#/components/schemas/OutboundWebhookContractedEventTypeV1'
      minItems: 8
      maxItems: 8
      uniqueItems: true
      x-webhook-event-contract-version: '1.0'
    OutboundWebhookContractedEventTypeV1:
      type: string
      description: Versioned event names with a defined payload contract. Only names also present in OutboundWebhookEventTypeV1
        are currently subscribable.
      enum:
      - alert.triggered
      - borme.daily
      - cartera.change
      - sanction.matched
      - segment.executed
      - tender.matched
      - tender.deadline
      - webhook.test
      x-webhook-event-contract-version: '1.0'
    OutboundWebhookCreateRequest:
      type: object
      required:
      - url
      - events
      properties:
        url:
          type: string
          format: uri
          pattern: ^https://
        events:
          $ref: '#/components/schemas/OutboundWebhookEventSubscriptionsV1'
      additionalProperties: false
    OutboundWebhookEventCatalogV1:
      description: Exact account-visible list of events with a currently wired durable producer.
      allOf:
      - $ref: '#/components/schemas/OutboundWebhookEventSubscriptionsV1'
      - type: array
        minItems: 3
        maxItems: 3
      x-webhook-event-contract-version: '1.0'
    OutboundWebhookEventSubscriptionsV1:
      type: array
      description: Events with a currently wired durable producer. Contracted-but-unwired event types
        are not subscribable. An empty input is normalized to alert.triggered; wildcard, inactive and
        unknown values are rejected.
      items:
        $ref: '#/components/schemas/OutboundWebhookEventTypeV1'
      maxItems: 3
      uniqueItems: true
      x-webhook-event-contract-version: '1.0'
    OutboundWebhookEventTypeV1:
      type: string
      description: Exact subscription allowlist for webhook payload contract 1.0. Unknown event types,
        wildcard subscriptions and unsupported schema versions fail closed. Adding an event requires an
        explicit runtime, OpenAPI and contract-test change so existing subscriptions never gain future
        events implicitly.
      enum:
      - alert.triggered
      - tender.matched
      - tender.deadline
      x-webhook-event-contract-version: '1.0'
      x-fail-closed: true
    OutboundWebhookOneTimeResponse:
      type: object
      required:
      - ok
      - webhook
      - secret_visible_once
      - idempotency_replayed
      - idempotency_expires_at
      properties:
        ok:
          type: boolean
          const: true
        webhook:
          type: object
          required:
          - id
          - secret
          - key_id
          properties:
            id:
              type: integer
              minimum: 1
            secret:
              type: string
              pattern: ^[a-f0-9]{64}$
              readOnly: true
              x-sensitive: true
              x-visible-once: true
              description: Visible only in the original mutation response and identical Idempotency-Key
                replays during the 24-hour recovery window; never returned by list/get.
            key_id:
              type: string
            events:
              $ref: '#/components/schemas/OutboundWebhookEventSubscriptionsV1'
          additionalProperties: false
        secret_visible_once:
          type: boolean
          const: true
        idempotency_replayed:
          type: boolean
          readOnly: true
        idempotency_expires_at:
          type: integer
          minimum: 1
          readOnly: true
          description: Unix timestamp for the encrypted replay window.
      additionalProperties: false
    OutboundWebhookUpdateRequest:
      type: object
      minProperties: 1
      properties:
        url:
          type: string
          format: uri
          pattern: ^https://
        events:
          $ref: '#/components/schemas/OutboundWebhookEventSubscriptionsV1'
        active:
          type: boolean
      additionalProperties: false
    WebhookUpdatedResponse:
      type: object
      required:
      - ok
      - webhook
      properties:
        ok:
          type: boolean
        webhook:
          $ref: '#/components/schemas/OutboundWebhook'
      additionalProperties: false
    WebhookWorkerHealth:
      type: object
      required:
      - available
      - status
      - reason
      properties:
        available:
          type: boolean
        status:
          type: string
        age_seconds:
          type:
          - integer
          - 'null'
        reason:
          type: string
      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).