OpenMercantil Billing API

Session-bound Stripe checkout, invoices and portal contracts. External actions are bounded, idempotent and never exposed through the public MCP.

OpenAPI Specification

openmercantil-billing-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: OpenMercantil Billing 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: Billing
  description: Session-bound Stripe checkout, invoices and portal contracts. External actions are bounded,
    idempotent and never exposed through the public MCP.
paths:
  /api/v1/donation:
    post:
      operationId: postDonationCheckout
      tags:
      - Billing
      summary: Create a one-time donation Checkout session
      description: Anonymous or authenticated. Body is capped at 16 KiB; Stripe work is fail-closed, budgeted
        by trusted client IP and idempotent within a 15-minute retry window. A supplied idempotency key
        is hashed and namespaced before Stripe.
      x-plane: billing
      security: []
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
          pattern: ^[A-Za-z0-9._:\-]{8,200}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - amount_cents
              properties:
                amount_cents:
                  type: integer
                  minimum: 100
                  maximum: 100000
                message:
                  type: string
                  maxLength: 500
                idempotency_key:
                  type: string
                  pattern: ^[A-Za-z0-9._:\-]{8,200}$
      responses:
        '200':
          description: Checkout URL and Stripe session id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          description: Request body too large
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action budget or Stripe unavailable
  /api/v1/credits/checkout:
    post:
      operationId: postCreditsCheckout
      tags:
      - Billing
      - User
      summary: Create an idempotent credit-pack Checkout session
      description: Requires login and CSRF. The body is capped at 16 KiB and every external attempt consumes
        a strict per-user budget.
      x-plane: billing
      security:
      - cookieAuth: []
      parameters:
      - $ref: '#/components/parameters/CsrfHeader'
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
          pattern: ^[A-Za-z0-9._:\-]{8,200}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - pack
              properties:
                pack:
                  type: string
                  enum:
                  - pack_250
                  - pack_600
                  - pack_1500
                  - pack_4000
                csrf:
                  type: string
                idempotency_key:
                  type: string
                  pattern: ^[A-Za-z0-9._:\-]{8,200}$
      responses:
        '200':
          description: Checkout URL and Stripe session id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '413':
          description: Request body too large
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action budget or Stripe unavailable
  /api/v1/checkout:
    post:
      operationId: postSubscriptionCheckout
      tags:
      - Billing
      - User
      summary: Create a subscription Checkout session
      description: 'Requires a Secure, SameSite browser session and X-CSRF-Token. Trial policy is server-owned:
        clients cannot send trial_days or no_default_trial. Idempotency is scoped to user, plan and billing
        period before Stripe.'
      x-plane: billing
      security:
      - cookieAuth: []
      parameters:
      - $ref: '#/components/parameters/CsrfHeader'
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
          pattern: ^[A-Za-z0-9._:\-]{8,200}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - plan
              properties:
                plan:
                  type: string
                  enum:
                  - profesional
                  - max
                  - pro
                  - business
                  - agency
                billing:
                  type: string
                  enum:
                  - monthly
                  - yearly
                  - anual
                  default: monthly
                coupon:
                  type: string
                  maxLength: 80
                csrf:
                  type: string
                  description: Body fallback accepted by the runtime; prefer X-CSRF-Token.
                idempotency_key:
                  type: string
                  pattern: ^[A-Za-z0-9._:\-]{8,200}$
      responses:
        '200':
          description: Checkout URL and Stripe session id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: Request body too large
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action budget or Stripe unavailable
        '403':
          description: Invalid CSRF token
  /api/v1/billing/invoices:
    get:
      operationId: getBillingInvoices
      tags:
      - Billing
      - User
      summary: List the authenticated user's invoices and subscription
      x-plane: billing
      security:
      - cookieAuth: []
      responses:
        '200':
          description: Local billing projection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingInvoicesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/billing/portal:
    get:
      operationId: getBillingPortal
      tags:
      - Billing
      - User
      summary: Redirect to the authenticated user's Stripe portal
      x-plane: billing
      security:
      - cookieAuth: []
      responses:
        '302':
          description: Redirect to Stripe Customer Portal
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action budget or Stripe unavailable
    post:
      operationId: postBillingPortal
      tags:
      - Billing
      - User
      summary: Create a Stripe portal session as JSON
      x-plane: billing
      security:
      - cookieAuth: []
      parameters:
      - $ref: '#/components/parameters/CsrfHeader'
      responses:
        '200':
          description: Stripe Customer Portal URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid CSRF token
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action budget or Stripe unavailable
  /api/v1/portal:
    post:
      operationId: postLegacyBillingPortal
      tags:
      - Billing
      - User
      summary: Create a Stripe Customer Portal session through the legacy alias
      description: Deprecated alias of `POST /api/v1/billing/portal`. Requires an authenticated browser
        session and a valid CSRF token supplied either in `X-CSRF-Token` or the form field `csrf`. New
        clients must use the canonical billing route.
      deprecated: true
      x-replaced-by: /api/v1/billing/portal
      x-plane: billing
      x-rate-limit: 'Per-user action budget: 5 attempts per 15 minutes and 30 per day'
      x-mcp-excluded: true
      security:
      - cookieAuth: []
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        description: Required unless the same token is sent in the form field `csrf`.
        schema:
          type: string
          minLength: 32
          maxLength: 256
      requestBody:
        required: false
        description: Legacy form fallback for CSRF. Prefer the header and an empty body.
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: false
              properties:
                csrf:
                  type: string
                  minLength: 32
                  maxLength: 256
      responses:
        '200':
          description: Stripe Customer Portal URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid or missing CSRF token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Action-budget storage or Stripe Customer Portal is unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/stripe-webhook:
    post:
      operationId: receiveStripeWebhook
      tags:
      - Billing
      - Integrations
      summary: Receive a signed Stripe event
      description: Provider callback, not an end-user API. The exact raw JSON body is capped at 512 KiB
        and verified against `Stripe-Signature` before durable inbox claim or side effects. Provider `event.id`
        supplies idempotency; terminal duplicates return 200, active leases return 503 for retry, and
        failures return 500 so Stripe retries. No webhook secret, signature, raw payload or internal exception
        is returned. This route is excluded from every public MCP surface.
      x-plane: integration-provider-callback
      x-provider: stripe
      x-max-body-bytes: 524288
      x-idempotency-key: Stripe event.id
      x-log-policy: Redact Stripe-Signature and the raw request body
      x-mcp-excluded: true
      security: []
      parameters:
      - name: Stripe-Signature
        in: header
        required: true
        description: Stripe-generated signature verified over the exact raw body. It is sensitive request
          metadata and must never be logged or echoed.
        schema:
          type: string
          minLength: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeWebhookEventRequest'
      responses:
        '200':
          description: Event accepted, deliberately ignored, or already terminal. All response bodies
            are non-secret acknowledgements.
          content:
            text/plain:
              schema:
                type: string
                enum:
                - ok
                - duplicate (terminal idempotent skip)
                - ignored (other source)
                - credits ok
                - donation ok
                - subscription missing
                - historical subscription ignored
                - clawback rejected
                - clawback
                - ignored
                - ignored invoice
                - ignored payment failure
        '400':
          description: Signature missing/invalid, or the raw request stream cannot be read
          content:
            text/plain:
              schema:
                type: string
                const: Bad signature
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: request_body_unreadable
        '413':
          description: Raw callback body exceeds 512 KiB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestBodyTooLargeError'
              example:
                error: request_body_too_large
                max_bytes: 524288
        '500':
          description: Inbox persistence or event processing failed; Stripe must retry
          content:
            text/plain:
              schema:
                type: string
                enum:
                - webhook inbox unavailable
                - error
        '503':
          description: Another worker owns an active lease or a signed OpenMercantil checkout cannot yet
            be resolved to its authoritative account owner; Stripe must retry
          headers:
            Retry-After:
              description: Retry delay in seconds
              schema:
                type: integer
                const: 5
          content:
            text/plain:
              schema:
                type: string
                enum:
                - event already processing
                - credits owner unavailable
                - subscription owner unavailable
components:
  headers:
    NoStoreCacheControl:
      description: Error responses must not be stored.
      schema:
        type: string
        const: no-store
  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:
    BadRequest:
      description: Invalid request
      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:
    BillingInvoice:
      type: object
      description: Authenticated user's invoice metadata; URLs point to Stripe-hosted documents.
      properties:
        id:
          type: integer
        stripe_invoice_id:
          type: string
        invoice_number:
          type:
          - string
          - 'null'
        amount_paid_cents:
          type:
          - integer
          - 'null'
        amount_due_cents:
          type:
          - integer
          - 'null'
        subtotal_cents:
          type:
          - integer
          - 'null'
        tax_cents:
          type:
          - integer
          - 'null'
        tax_rate_pct:
          type:
          - number
          - 'null'
        currency:
          type:
          - string
          - 'null'
        status:
          type:
          - string
          - 'null'
        invoice_pdf:
          type:
          - string
          - 'null'
          format: uri
        hosted_invoice_url:
          type:
          - string
          - 'null'
          format: uri
        period_start:
          type:
          - string
          - 'null'
        period_end:
          type:
          - string
          - 'null'
        paid_at:
          type:
          - string
          - 'null'
        created_at:
          type: string
      additionalProperties:
        $ref: '#/components/schemas/JsonValue'
    BillingInvoicesResponse:
      type: object
      required:
      - ok
      - subscription
      - invoices
      properties:
        ok:
          type: boolean
        subscription:
          anyOf:
          - $ref: '#/components/schemas/BillingSubscription'
          - type: 'null'
        invoices:
          type: array
          items:
            $ref: '#/components/schemas/BillingInvoice'
          maxItems: 50
      additionalProperties: false
    BillingSubscription:
      type: object
      description: Authenticated user's current subscription metadata.
      properties:
        id:
          type: integer
        stripe_subscription_id:
          type: string
        stripe_customer_id:
          type:
          - string
          - 'null'
        plan:
          type: string
        billing:
          type:
          - string
          - 'null'
        amount_cents:
          type:
          - integer
          - 'null'
        currency:
          type:
          - string
          - 'null'
        status:
          type: string
        current_period_start:
          type:
          - string
          - 'null'
        current_period_end:
          type:
          - string
          - 'null'
        cancel_at_period_end:
          type:
          - integer
          - boolean
        canceled_at:
          type:
          - string
          - 'null'
        created_at:
          type: string
        updated_at:
          type: string
      additionalProperties:
        $ref: '#/components/schemas/JsonValue'
    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'
    RequestBodyTooLargeError:
      type: object
      required:
      - error
      - max_bytes
      properties:
        error:
          type: string
          const: request_body_too_large
        max_bytes:
          type: integer
          minimum: 1
      additionalProperties: false
    StripeSessionResponse:
      type: object
      required:
      - url
      - session_id
      properties:
        url:
          type: string
          format: uri
        session_id:
          type: string
      additionalProperties: false
    StripeWebhookEventRequest:
      type: object
      description: Stripe Event envelope. The exact signed raw bytes, not a reserialized object, are used
        for verification. Provider extension fields remain typed JSON and are not reflected to callers.
      required:
      - id
      - type
      - data
      properties:
        id:
          type: string
          minLength: 1
          description: Provider event id and durable idempotency key.
        object:
          type: string
          const: event
        type:
          type: string
          minLength: 1
        created:
          type: integer
          minimum: 0
        livemode:
          type: boolean
        api_version:
          type:
          - string
          - 'null'
        data:
          type: object
          required:
          - object
          properties:
            object:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/JsonValue'
            previous_attributes:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/JsonValue'
          additionalProperties:
            $ref: '#/components/schemas/JsonValue'
      additionalProperties:
        $ref: '#/components/schemas/JsonValue'
  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).