openapi: 3.1.0
info:
  title: POSnavigator Bank Object API
  version: "1.0.0"
  description: |-
    Canonical machine-readable contract for reading and partially updating
    a bank record. An agent given only this URL and a valid API key can list banks,
    fetch one bank, and PATCH landing SEO, FAQ, and device-landing copy.
    
    This API does not export the product/fee hierarchy and does not update
    data-freshness timestamps. Those live on GET /openapi/provider-research.yaml
    (getBankExport, listBankDataFreshness, patchBankDataFreshness). CRM contacts
    live on GET /openapi/provider-crm.yaml.
    
    # What this API is for
    
    A Bank is a payment-provider record. GET returns the same object the admin bank
    form shows, minus nested products. PATCH is a partial merge: translations and
    landing.seo / landing.content locale objects merge; landing FAQ arrays replace
    wholesale when sent.
    
    # Access
    
    Any valid key can call these operations. Admin keys (`isAdmin: true` on the
    Mongo user) can read and update any bank. Non-admin keys can only access banks
    listed in the user's `banks` array. Admin-only fields on PATCH: landing,
    device_landing, slug, isprod, istestbank, isbankedit, isorderable,
    isaccountholder, transaction_template, social_urls, data_verification_urls,
    data_verification_instructions, last_data_verification_ai_at,
    last_data_verification_human_at, physical_device_sales_enabled,
    device_fulfillment_policy. Sending one of those with a non-admin key returns
    403 FORBIDDEN.
    
    # The happy path
    
    1. GET /openapi/banks.yaml — this document, public.
    2. GET /api/v1/banks — discover bank ids the key can see (includes CRM summary).
    3. GET /api/v1/banks/{bankId} — round-trip the object.
    4. PATCH only the fields you intend to change. Re-GET if you need the merged result.
    
    Send `X-Api-Key: <key>` on every operation except this document itself.
    Create a key at /user/api-keys. Format: `pn_` plus a random string. Copy it
    immediately; it is shown once. Default new keys include blog, legal, presetfilter,
    versus-landing, widget, agent-analytics and hero-analytics scopes. Missing scopes
    return 403 INSUFFICIENT_PERMISSIONS. Bank-scoped resources also require the key
    owner to be admin or listed on that bank; otherwise 403 FORBIDDEN.
    
    # Response envelope
    
    Success: `{ "success": true, "data": ... }`.
    Failure: `{ "success": false, "error": { "code", "message" } }`.
    Branch on error.code, never on error.message.
    
    # Error codes
    
    | code | status | what to do |
    | --- | --- | --- |
    | MISSING_API_KEY | 401 | add X-Api-Key; do not retry blindly |
    | INVALID_API_KEY | 401 | stop; ask a human for a new key |
    | INSUFFICIENT_PERMISSIONS | 403 | stop; this key lacks a required scope |
    | FORBIDDEN | 403 | stop or switch to a key with bank/admin access |
    | RATE_LIMIT_EXCEEDED | 429 | wait Retry-After seconds |
    | INTERNAL_ERROR | 500 | retry with backoff |
    
    # Rate limiting
    
    Every response after successful authentication carries X-RateLimit-Limit,
    X-RateLimit-Remaining and X-RateLimit-Reset. A 429 also carries Retry-After.
    The two 401s are emitted before rate-limit accounting. Default window is 100
    requests per rolling hour unless the key was configured otherwise.
    
    # Canonical host
    
    Use https://posnavigator.eu. Legacy .hu hosts redirect; authenticated writes
    should call .eu directly.
    
    Additional codes: INVALID_BANK_ID 400, INVALID_JSON 400, VALIDATION_ERROR 422,
    BANK_NOT_FOUND 404.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: Banks
security:
  - ApiKeyAuth: []
paths:
  /openapi/banks.yaml:
    get:
      operationId: getBanksOpenApi
      tags: [Discovery]
      security: []
      summary: Public OpenAPI 3.1 description
      description: |-
        This document. Public: no API key, no rate limit. Fetch it first and treat
        it as the complete manual. Every schema, limit, example, error code and
        header an agent needs is here. Linked from GET /openapi/catalog.yaml.
        Data operations below require authentication as described in info.
      responses:
        '200':
          description: OpenAPI YAML
          content:
            application/yaml:
              schema:
                type: string
              example: 'openapi: 3.1.0'
  /api/v1/banks:
    get:
      operationId: listBanks
      tags: [Banks]
      summary: List banks this API key can access
      description: |-
        Returns every bank the key owner may see, sorted for admin dashboards.
        Admin keys get all banks; other keys get only banks in the user's banks
        array. Each row includes dba_name, slug, isprod, and CRM summary columns
        (crm_status, contact/agreement/contract counts, missing-relation flags).
        Not paginated. Use this to discover bankId values before GET/PATCH.
        Side-effect free.
      responses:
        '200':
          description: Bank list
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankListResponse'
              example:
                success: true
                data:
                  - _id: '507f1f77bcf86cd799439011'
                    dba_name: Example Bank
                    company_name: Example Bank Ltd.
                    slug: example-bank
                    isprod: true
                    crm_status: ACTIVE
                    crm_contacts_count: 2
                    crm_agreements_count: 1
                    crm_contracts_count: 1
                    crm_missing_relations:
                      contacts: false
                      agreements: false
                      contracts: false
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/v1/banks/{bankId}:
    parameters:
      - $ref: '#/components/parameters/BankId'
    get:
      operationId: getBank
      tags: [Banks]
      summary: Get one bank record
      description: |-
        Returns the serialized bank object: identity, translations, landing SEO
        and FAQ, device_landing, fulfillment flags, verification URLs, and CRM
        timestamps. Does not include nested mainservices or products — use
        getBankExport on the provider-research spec for the hierarchy. Round-trip
        this object when preparing a PATCH so you do not drop locale keys.
      responses:
        '200':
          description: Bank object
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankDetailResponse'
              example:
                success: true
                data:
                  _id: '507f1f77bcf86cd799439011'
                  dba_name: Example Bank
                  slug: example-bank
                  isprod: true
                  website: 'https://example.com'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    patch:
      operationId: patchBank
      tags: [Banks]
      summary: Partially update a bank
      description: |-
        Merges the JSON body into the stored bank. Locale objects under
        translations and landing.seo / landing.content merge per language; if you
        send landing.content.hu.faqItems the whole FAQ array for that locale is
        replaced. Admin-only fields are listed in the info runbook — a non-admin
        key sending them gets 403. After a successful write the public bank
        landing paths are revalidated. Prefer GET then PATCH of the fields you
        changed rather than reconstructing the document by hand.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BankPatchRequest'
            example:
              landing:
                seo:
                  hu:
                    metaTitle: Example Bank POS terminal 2026
                    metaDescription: Hasonlítsd össze az Example Bank POS szolgáltatásait.
      responses:
        '200':
          description: Updated bank
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankDetailResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |-
        Send `X-Api-Key: pn_...` on every data operation. Create a key at
        /user/api-keys; it is shown once. Inactive or expired keys return 401.
  headers:
    X-RateLimit-Limit:
      description: Maximum requests allowed in the current window
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix epoch seconds when the window resets
      schema:
        type: integer
    Retry-After:
      description: Seconds to wait before retrying a 429
      schema:
        type: integer
  parameters:
    BankId:
      name: bankId
      in: path
      required: true
      description: 24-character hex Mongo ObjectId of the bank
      schema:
        type: string
        pattern: '^[a-fA-F0-9]{24}$'
        maxLength: 24

  schemas:
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              maxLength: 80
              description: Stable machine code. Branch on this, never on message.
            message:
              type: string
              maxLength: 2000
            details:
              type: array
              maxItems: 50
              items:
                type: string
                maxLength: 500
            retryAfter:
              type: integer
              description: Present on RATE_LIMIT_EXCEEDED
            field_errors:
              type: object
              additionalProperties:
                type: array
                maxItems: 20
                items:
                  type: string
                  maxLength: 500
    LocalePair:
      type: object
      additionalProperties: false
      properties:
        hu:
          type: string
          maxLength: 20000
        en:
          type: string
          maxLength: 20000
    OffsetPagination:
      type: object
      additionalProperties: false
      required: [total, limit, offset, hasMore]
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean
    BankListItem:
      type: object
      additionalProperties: true
      required: [_id, dba_name, company_name, slug, isprod]
      properties:
        _id:
          type: string
          maxLength: 24
        dba_name:
          type: string
          maxLength: 200
        company_name:
          type: string
          maxLength: 200
        slug:
          type: string
          maxLength: 120
        isprod:
          type: boolean
        crm_status:
          type: string
          enum: [NEW, CONTACTED, MEETING_SCHEDULED, NEGOTIATING, ACTIVE, ON_HOLD, CLOSED_LOST]
          description: NEW first touch. CONTACTED outreach started. MEETING_SCHEDULED a meeting exists. NEGOTIATING commercial discussion. ACTIVE live relationship. ON_HOLD paused. CLOSED_LOST lost.
        crm_primary_contact_name:
          type: string
          maxLength: 200
        crm_contacts_count:
          type: integer
        crm_agreements_count:
          type: integer
        crm_contracts_count:
          type: integer
        crm_missing_relations:
          type: object
          additionalProperties: false
          properties:
            contacts: { type: boolean }
            agreements: { type: boolean }
            contracts: { type: boolean }
    BankListResponse:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          maxItems: 5000
          items:
            $ref: '#/components/schemas/BankListItem'
    BankDetailResponse:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          additionalProperties: true
          description: Serialized bank. Treat unknown fields as read-only unless listed on BankPatchRequest.
          properties:
            _id:
              type: string
              maxLength: 24
            dba_name:
              type: string
              maxLength: 200
            website:
              type: string
              maxLength: 500
            slug:
              type: string
              maxLength: 120
            isprod:
              type: boolean
            landing:
              type: object
              additionalProperties: true
            device_landing:
              type: object
              additionalProperties: true
    BankPatchRequest:
      type: object
      additionalProperties: true
      description: Partial bank update. Only send fields you intend to change.
      properties:
        dba_name:
          type: string
          maxLength: 200
          description: Display name
        company_name:
          type: string
          maxLength: 200
          description: Legal name
        website:
          type: string
          maxLength: 500
          description: Primary website
        email:
          type: string
          maxLength: 200
        phone:
          type: string
          maxLength: 50
        landing:
          type: object
          additionalProperties: true
          description: Admin-only. SEO, content, FAQ. Locale objects merge; faqItems replace.
        device_landing:
          type: object
          additionalProperties: true
          description: Admin-only terminal landing copy, preset ids, CTAs, social proof
        physical_device_sales_enabled:
          type: boolean
          description: Admin-only. Enables physical terminal sales on this bank
        device_fulfillment_policy:
          type: object
          additionalProperties: false
          description: Admin-only KYC/bank/admin review and test-mode flags
          properties:
            kyc_review_required: { type: boolean }
            bank_approval_required: { type: boolean }
            admin_review_required: { type: boolean }
            test_mode_enabled: { type: boolean }

