openapi: 3.0.3
info:
  title: ERPly Pro PSFE API
  version: 0.1.0-dev
  description: 'REST API for the ERPly Pro PSFE (Proveedor de Servicios de Facturación

    Electrónica) covering issuance, polling, reception, certification, and

    operational status. All write operations follow RFC 9457 (`application/problem+json`)

    for error responses and are idempotent via the `Idempotency-Key` header.


    Errors share the `Problem` schema; consult [`/errors`](https://dev.api.erply.pro/errors)

    for the full DGII error dictionary.

    '
  contact:
    name: ERPly Pro Support
    url: https://dev.api.erply.pro/docs/support
    email: soporte@erply.pro
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
externalDocs:
  description: Error codes reference
  url: https://dev.api.erply.pro/errors
servers:
- url: https://api.erply.pro
  description: Production
- url: https://staging.api.erply.pro
  description: Staging
- url: https://dev.api.erply.pro
  description: Development sandbox
- url: https://sandbox.api.erply.pro
  description: Public sandbox (rate-limited demo key)
tags:
- name: Companies
  description: Tenant company configuration.
- name: Certification
  description: DGII set-de-pruebas certification orchestration.
- name: Issuance
  description: e-CF issuance.
- name: Status Polling
  description: DGII validation status retrieval.
- name: Communication
  description: Email/PDF delivery.
- name: Reception
  description: Inbound documents addressed to the tenant.
- name: Approvals
  description: Commercial approvals from buyers.
- name: Reporting
  description: Historical totals.
- name: System Health
  description: DGII operational status.
- name: Identity Validation
  description: RNC/Cédula lookup and prefix search against the centralised DGII registry.
security:
- bearerAuth: []
  apiKeyAuth: []
paths:
  /v1/company:
    post:
      operationId: createCompany
      tags:
      - Companies
      summary: Register a new tenant company
      security:
      - apiKeyAuth: []
      description: 'Registers a new tenant company within the middleware, securely

        configuring its RNC and initial environment variables. The RNC must

        be valid per DGII registry; the response returns the canonical

        `tenantId` used for all subsequent calls.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCompanyRequest'
            examples:
              demo:
                $ref: '#/components/examples/CreateCompanyDemo'
      responses:
        '201':
          description: Company created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/company/{id}:
    parameters:
    - $ref: '#/components/parameters/TenantId'
    get:
      operationId: getCompany
      tags:
      - Companies
      summary: Fetch company configuration
      description: Retrieves configuration for an existing tenant.
      responses:
        '200':
          description: Company.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    patch:
      operationId: patchCompany
      tags:
      - Companies
      summary: Update company configuration
      description: Updates webhook URLs, base64 PDF logo, and other tenant config.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchCompanyRequest'
      responses:
        '200':
          description: Updated company.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/check-set-tests:
    post:
      operationId: createCheckSetTests
      tags:
      - Certification
      summary: Initiate a DGII set-de-pruebas
      description: 'Initiates the generation of the mandatory test set required by DGII

        to certify a company as an electronic issuer.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCheckSetTestsRequest'
      responses:
        '202':
          description: Test set submitted; poll status with `getCheckSetTests`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckSetTests'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/check-set-tests/{id}/idCompany/{idCompany}:
    parameters:
    - $ref: '#/components/parameters/SetTestId'
    - $ref: '#/components/parameters/IdCompany'
    get:
      operationId: getCheckSetTests
      tags:
      - Certification
      summary: Query set-de-pruebas status
      description: Queries the validation status of a submitted test set.
      responses:
        '200':
          description: Test-set status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckSetTests'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/provider-info:
    get:
      operationId: getProviderInfo
      tags:
      - Certification
      summary: PSFE provider info
      description: Retrieves the registered information of the certified PSFE provider (ERPly Pro).
      responses:
        '200':
          description: Provider info.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices:
    post:
      operationId: postInvoice
      tags:
      - Issuance
      summary: Emit an e-CF
      description: 'Emite un comprobante fiscal electrónico (e-CF) firmado XAdES-BES y lo

        entrega a la DGII. Soporta los tipos 31 (Crédito Fiscal), 32 (Consumo),

        33 (Nota de Débito), 34 (Nota de Crédito), 41 (Compras), y 44 (Régimen

        Especial). Esta operación es **idempotente** vía el header

        `Idempotency-Key`: la misma clave devuelve la respuesta original

        durante 24 horas.


        ---


        Issues a signed e-CF (XAdES-BES) and submits it to DGII. Supported

        types: 31 (Fiscal Credit), 32 (Consumption), 33 (Debit Note),

        34 (Credit Note), 41 (Purchases), 44 (Special Regime).

        '
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceRequest'
            examples:
              type31:
                $ref: '#/components/examples/InvoiceType31'
              type32:
                $ref: '#/components/examples/InvoiceType32'
      responses:
        '200':
          description: Replay (matches a previous Idempotency-Key).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceAccepted'
        '202':
          description: Submitted; poll status via `getFiscalInvoice` or `getInvoiceByCompany`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '504':
          $ref: '#/components/responses/GatewayTimeout'
      x-codeSamples:
      - lang: curl
        label: curl
        source: "curl -X POST https://sandbox.api.erply.pro/v1/invoices \\\n  -H \"Authorization: Bearer\
          \ $JWT\" \\\n  -H \"x-api-key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n \
          \ -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d @invoice.json\n"
      - lang: python
        label: Python (httpx)
        source: "import os, uuid, httpx\nr = httpx.post(\n    \"https://sandbox.api.erply.pro/v1/invoices\"\
          ,\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['JWT']}\",\n        \"x-api-key\"\
          : os.environ[\"API_KEY\"],\n        \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json=payload,\n\
          \    timeout=10,\n)\nr.raise_for_status()\n"
      - lang: javascript
        label: Node (axios)
        source: "import axios from \"axios\";\nimport { randomUUID } from \"node:crypto\";\nawait axios.post(\"\
          https://sandbox.api.erply.pro/v1/invoices\", payload, {\n  headers: {\n    Authorization: `Bearer\
          \ ${process.env.JWT}`,\n    \"x-api-key\": process.env.API_KEY,\n    \"Idempotency-Key\": randomUUID(),\n\
          \  },\n  timeout: 10_000,\n});\n"
      - lang: python
        label: Odoo (requests)
        source: "import uuid, requests\nself.env[\"ir.config_parameter\"].sudo().get_param(\"erplypro.endpoint\"\
          )\nrequests.post(\n    f\"{endpoint}/v1/invoices\",\n    headers={\n        \"Authorization\"\
          : f\"Bearer {jwt}\",\n        \"x-api-key\": api_key,\n        \"Idempotency-Key\": str(uuid.uuid4()),\n\
          \    },\n    json=payload,\n    timeout=10,\n).raise_for_status()\n"
  /v1/fiscal-invoices/{id}:
    parameters:
    - $ref: '#/components/parameters/InvoiceId'
    get:
      operationId: getFiscalInvoice
      tags:
      - Status Polling
      summary: Get fiscal invoice (Type 31) status
      description: Retrieves the DGII validation status of a Fiscal Credit e-CF (Type 31).
      responses:
        '200':
          description: Fiscal invoice status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
      x-codeSamples:
      - lang: curl
        label: curl
        source: "curl https://sandbox.api.erply.pro/v1/fiscal-invoices/$ID \\\n  -H \"Authorization: Bearer\
          \ $JWT\" -H \"x-api-key: $API_KEY\"\n"
      - lang: python
        label: Python (httpx)
        source: 'import httpx

          r = httpx.get(f"{base}/v1/fiscal-invoices/{id}", headers=headers, timeout=5)

          '
      - lang: javascript
        label: Node (axios)
        source: 'const r = await axios.get(`${base}/v1/fiscal-invoices/${id}`, { headers });

          '
      - lang: python
        label: Odoo (requests)
        source: 'requests.get(f"{base}/v1/fiscal-invoices/{id}", headers=headers, timeout=5)

          '
  /v1/invoices/{id}/idCompany/{idCompany}:
    parameters:
    - $ref: '#/components/parameters/InvoiceId'
    - $ref: '#/components/parameters/IdCompany'
    get:
      operationId: getInvoiceByCompany
      tags:
      - Status Polling
      summary: Get consumption invoice (Type 32) status
      description: 'Retrieves the validation status of standard Consumption e-CFs (Type 32),

        utilising the company ID for precise multi-tenant context routing.

        '
      responses:
        '200':
          description: Invoice status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/fiscal-invoices/notify-by-email:
    post:
      operationId: notifyFiscalInvoiceByEmail
      tags:
      - Communication
      summary: Email XML/PDF containers to recipient
      description: 'Triggers the middleware to generate XML and PDF containers for a

        previously emitted e-CF and email them to the recipient.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotifyByEmailRequest'
      responses:
        '202':
          description: Notification queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/received-documents:
    get:
      operationId: listReceivedDocuments
      tags:
      - Reception
      summary: List inbound e-CFs
      description: Lists inbound documents where the tenant acts as the buyer.
      parameters:
      - in: query
        name: from
        schema:
          type: string
          format: date
        description: Start of date range (inclusive).
      - in: query
        name: to
        schema:
          type: string
          format: date
        description: End of date range (inclusive).
      - in: query
        name: rnc
        schema:
          type: string
          pattern: ^[0-9]{9,11}$
        description: Issuer RNC filter.
      - in: query
        name: limit
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
      - in: query
        name: cursor
        schema:
          type: string
      responses:
        '200':
          description: Paginated received documents.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedDocumentsPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/received-documents/{id}:
    parameters:
    - $ref: '#/components/parameters/ReceivedDocumentId'
    get:
      operationId: getReceivedDocument
      tags:
      - Reception
      summary: Fetch a received document
      description: Retrieves details and the XML payload of an individual received e-CF.
      responses:
        '200':
          description: Received document.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedDocument'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/reception/dgii/receive-document:
    post:
      operationId: receiveDgiiDocument
      tags:
      - Reception
      summary: Inbound DGII reception webhook
      description: Webhook or manual trigger to handle inbound reception of an e-CF directly from DGII.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReceivedDocument'
      responses:
        '202':
          description: Document accepted for processing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceiveAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/received-commercial-approvals/{id}:
    parameters:
    - $ref: '#/components/parameters/InvoiceId'
    get:
      operationId: getCommercialApproval
      tags:
      - Approvals
      summary: Query commercial approval status
      description: Queries the status of commercial approvals from buyers for previously issued e-CFs.
      responses:
        '200':
          description: Commercial-approval status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialApproval'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{id}/emitted-documents:
    parameters:
    - $ref: '#/components/parameters/TenantId'
    get:
      operationId: getCompanyEmittedDocuments
      tags:
      - Reporting
      summary: Total emitted documents for a tenant
      description: Retrieves the historical total of e-CFs successfully emitted by a tenant.
      parameters:
      - in: query
        name: from
        schema:
          type: string
          format: date
      - in: query
        name: to
        schema:
          type: string
          format: date
      responses:
        '200':
          description: Emitted-document totals.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmittedDocumentsReport'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/check-dgii-status/idCompany/{idCompany}:
    parameters:
    - $ref: '#/components/parameters/IdCompany'
    get:
      operationId: getCheckDgiiStatus
      tags:
      - System Health
      summary: DGII operational status
      description: 'Queries the current operational status and maintenance windows of the

        DGII web services so callers can pre-emptively handle transmission

        timeouts.

        '
      responses:
        '200':
          description: DGII status snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DgiiStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
      x-codeSamples:
      - lang: curl
        label: curl
        source: "curl https://sandbox.api.erply.pro/v1/check-dgii-status/idCompany/$ID \\\n  -H \"Authorization:\
          \ Bearer $JWT\" -H \"x-api-key: $API_KEY\"\n"
      - lang: python
        label: Python (httpx)
        source: 'import httpx

          r = httpx.get(f"{base}/v1/check-dgii-status/idCompany/{id}", headers=headers, timeout=5)

          '
      - lang: javascript
        label: Node (axios)
        source: 'const r = await axios.get(`${base}/v1/check-dgii-status/idCompany/${id}`, { headers });

          '
      - lang: python
        label: Odoo (requests)
        source: 'requests.get(f"{base}/v1/check-dgii-status/idCompany/{id}", headers=headers, timeout=5)

          '
  /v1/dgii/document/{document}:
    get:
      operationId: getDocument
      tags:
      - Identity Validation
      summary: Lookup a single taxpayer by RNC or Cédula
      description: 'Returns the DGII registry entry for the given RNC (9 or 11 digits) or Cédula (11 digits).
        Accepts with or without hyphens.

        Response is CDN-cacheable (Cache-Control: public, max-age=300).

        '
      parameters:
      - name: document
        in: path
        required: true
        description: RNC (9 or 11 digits) or Cédula (11 digits with optional hyphens).
        schema:
          type: string
          pattern: ^\d{9,11}$|^\d{3}-\d{7}-\d{1}$
      responses:
        '200':
          description: Registry entry found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistryRow'
        '400':
          description: Invalid document format.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '404':
          description: No registry entry for this document.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
      x-codeSamples:
      - lang: shell
        label: cURL
        source: 'curl -s https://api.erply.pro/v1/dgii/document/130862346 -H "x-api-key: $KEY"

          '
      - lang: python
        label: Python (requests)
        source: 'requests.get(f"{base}/v1/dgii/document/130862346", headers=headers, timeout=10)

          '
      - lang: javascript
        label: Node.js (fetch)
        source: 'await fetch(`${base}/v1/dgii/document/130862346`, { headers })

          '
  /v1/dgii/document\:search:
    get:
      operationId: searchDocuments
      tags:
      - Identity Validation
      summary: Search taxpayers by name, trade name, economic activity, or document prefix
      description: 'Prefix search across the DGII registry. Minimum query length is 3 characters.

        Results are normalised (uppercase, no accents). The `partial` flag indicates

        more results exist beyond the limit.

        '
      parameters:
      - name: q
        in: query
        required: true
        description: Search text (minimum 3 characters).
        schema:
          type: string
          minLength: 3
      - name: field
        in: query
        required: true
        description: Field to search.
        schema:
          type: string
          enum:
          - name
          - trade_name
          - economic_activity
          - document
          - any
      - name: limit
        in: query
        required: false
        description: Maximum results (1–100, default 25).
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
      responses:
        '200':
          description: Search results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResult'
        '400':
          description: Invalid parameters (query too short or invalid field).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
      x-codeSamples:
      - lang: shell
        label: cURL
        source: 'curl -s "https://api.erply.pro/v1/dgii/document:search?q=ERPLY&field=name&limit=5" -H
          "x-api-key: $KEY"

          '
      - lang: python
        label: Python (requests)
        source: 'requests.get(f"{base}/v1/dgii/document:search", params={"q": "ERPLY", "field": "name",
          "limit": 5}, headers=headers, timeout=10)

          '
      - lang: javascript
        label: Node.js (fetch)
        source: 'await fetch(`${base}/v1/dgii/document:search?q=ERPLY&field=name&limit=5`, { headers })

          '
  /v1/dgii/registry/status:
    get:
      operationId: getRegistryStatus
      tags:
      - Identity Validation
      summary: Get DGII registry freshness and ingest metadata
      description: 'Returns the timestamp and record count of the latest successful daily ingest.

        Useful for validating data freshness before bulk operations.

        If `stale` is true, the registry has not been updated in over 36 hours.

        '
      responses:
        '200':
          description: Registry status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistryStatus'
      x-codeSamples:
      - lang: shell
        label: cURL
        source: 'curl -s https://api.erply.pro/v1/dgii/registry/status -H "x-api-key: $KEY"

          '
      - lang: python
        label: Python (requests)
        source: 'requests.get(f"{base}/v1/dgii/registry/status", headers=headers, timeout=10)

          '
      - lang: javascript
        label: Node.js (fetch)
        source: 'await fetch(`${base}/v1/dgii/registry/status`, { headers })

          '
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Cognito-issued access token (custom Lambda authoriser; 60 s API GW result cache).
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: 'Per-tenant API key. Production callers MUST also send a signed

        `X-ErplyPro-Signature: t=<unix>,v1=<hmac-sha256-hex>` header (±300 s skew);

        see `x-erplypro-signature-spec` extension and `/docs/concepts#hmac`.

        '
      x-erplypro-signature-spec: https://api.erply.pro/docs/concepts#hmac
  parameters:
    TenantId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9-]{3,64}$
      description: Canonical tenant ID.
      example: tenant-130000000
    IdCompany:
      name: idCompany
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9-]{3,64}$
      description: Multi-tenant routing key.
      example: tenant-130000000
    InvoiceId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9-]{1,64}$
      description: Internal invoice identifier returned by `postInvoice`.
      example: encf-1a2b3c4d
    ReceivedDocumentId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9-]{1,64}$
    SetTestId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9-]{1,64}$
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        format: uuid
      description: 'Client-generated UUID. The same key replays the original response for

        24 hours; mismatched payloads return `409 Conflict`.

        '
  responses:
    BadRequest:
      description: Validation failed.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: Resource not found.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: Conflict with existing resource or idempotency mismatch.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    UnprocessableEntity:
      description: Business-rule violation (e.g., DGII rejected, math discrepancy).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    GatewayTimeout:
      description: Upstream DGII unavailable.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    InternalServerError:
      description: Unhandled server error.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  examples:
    CreateCompanyDemo:
      summary: Demo company registration
      value:
        rnc: '130000000'
        name: Demo Compañía S.R.L.
        webhookUrl: https://demo.example/erplypro/webhook
    InvoiceType31:
      summary: Type 31 — Fiscal credit (canonical fixture)
      value:
        type: '31'
        encf: E310000000001
        client_request_id: req-2026-05-01-001
        company:
          rnc: '131000001'
          name: ERPly Pro Demo S.R.L.
        customer:
          rnc: '131000002'
          name: Cliente Demo S.R.L.
        lines:
        - line_number: 1
          description: Servicio de consultoria
          quantity: '1'
          unit_price: '5000.00'
          subtotal: '5000.00'
        totals:
          subtotal: '5000.00'
          tax: '900.00'
          grand_total: '5900.00'
    InvoiceType32:
      summary: Type 32 — Consumption (issuer + recipient still required)
      value:
        type: '32'
        encf: E320000000001
        client_request_id: req-2026-05-01-002
        company:
          rnc: '131000001'
          name: ERPly Pro Demo S.R.L.
        customer:
          rnc: '131000003'
          name: Cliente Final S.R.L.
        lines:
        - line_number: 1
          description: Producto de consumo
          quantity: '2'
          unit_price: '250.00'
          subtotal: '500.00'
        totals:
          subtotal: '500.00'
          tax: '90.00'
          grand_total: '590.00'
  schemas:
    Problem:
      type: object
      description: RFC 9457 problem details. Returned for every 4xx and 5xx response.
      additionalProperties: true
      required:
      - type
      - title
      - status
      properties:
        type:
          type: string
          format: uri
          example: https://errors.api.erply.pro/validation-error
        title:
          type: string
          example: Validation failed
        status:
          type: integer
          minimum: 400
          maximum: 599
          example: 400
        detail:
          type: string
          example: lines[0].qty must be a positive number
        instance:
          type: string
          format: uri
          example: https://api.erply.pro/v1/invoices
        traceId:
          type: string
          example: 1-65fa7c3a-6f9c2d8e0a1b2c3d4e5f6789
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
    Company:
      type: object
      required:
      - tenantId
      - rnc
      - name
      - status
      properties:
        tenantId:
          type: string
          example: tenant-130000000
        rnc:
          type: string
          example: '130000000'
        name:
          type: string
          example: Demo Compañía S.R.L.
        webhookUrl:
          type: string
          format: uri
          nullable: true
        status:
          type: string
          enum:
          - active
          - suspended
          - pending
        createdAt:
          type: string
          format: date-time
    CreateCompanyRequest:
      type: object
      required:
      - rnc
      - name
      properties:
        rnc:
          type: string
          pattern: ^[0-9]{9,11}$
        name:
          type: string
          minLength: 3
          maxLength: 200
        webhookUrl:
          type: string
          format: uri
    PatchCompanyRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 200
        webhookUrl:
          type: string
          format: uri
        logoBase64:
          type: string
          format: byte
          description: Optional base64-encoded PNG/JPEG logo, ≤ 200 KB.
    CreateCheckSetTestsRequest:
      type: object
      required:
      - tenantId
      properties:
        tenantId:
          type: string
          example: tenant-130000000
    CheckSetTests:
      type: object
      required:
      - id
      - tenantId
      - status
      properties:
        id:
          type: string
          example: settest-1234
        tenantId:
          type: string
          example: tenant-130000000
        status:
          type: string
          enum:
          - submitted
          - processing
          - accepted
          - rejected
        submittedAt:
          type: string
          format: date-time
    ProviderInfo:
      type: object
      required:
      - providerName
      - providerRnc
      - certifiedAt
      properties:
        providerName:
          type: string
          example: ERPly Pro S.R.L.
        providerRnc:
          type: string
          example: '130000000'
        certifiedAt:
          type: string
          format: date
        certificationNumber:
          type: string
          example: PSFE-2026-001
    InvoiceLine:
      type: object
      description: 'Line item. Decimal fields are accepted as JSON strings to avoid

        client-side rounding error; the server validates that

        ``quantity * unit_price == subtotal ± 0.01`` (DGII rule).

        '
      required:
      - line_number
      - description
      - quantity
      - unit_price
      - subtotal
      properties:
        line_number:
          type: integer
          minimum: 1
          maximum: 999
        description:
          type: string
          minLength: 1
          maxLength: 80
        quantity:
          type: string
          pattern: ^[0-9]+(\.[0-9]+)?$
          example: '1'
        unit_price:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,2})?$
          example: '5000.00'
        subtotal:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,2})?$
          example: '5000.00'
    InvoiceParty:
      type: object
      description: Issuer (`company`) or recipient (`customer`) identification.
      required:
      - rnc
      - name
      properties:
        rnc:
          type: string
          pattern: ^[0-9]{9}([0-9]{2})?$
        name:
          type: string
          minLength: 1
          maxLength: 150
    InvoiceTotals:
      type: object
      required:
      - subtotal
      - tax
      - grand_total
      properties:
        subtotal:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,2})?$
        tax:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,2})?$
        grand_total:
          type: string
          pattern: ^[0-9]+(\.[0-9]{1,2})?$
    InvoiceRequest:
      type: object
      description: 'Inbound invoice payload for `POST /v1/invoices`. Mirrors the

        Pydantic `InvoicePayload` model in `lambdas.shared.models.invoice`.

        Tenant identity is **NOT** in the body — it is derived from the

        JWT claims (`tenant_id`, `environment`) by `TenantContext.from_event`.

        See BUG-EP-03 (bugs.md) for the previous OpenAPI/Pydantic drift.

        '
      required:
      - type
      - encf
      - client_request_id
      - company
      - customer
      - lines
      - totals
      properties:
        type:
          type: string
          enum:
          - '31'
          - '32'
          - '33'
          - '34'
          - '41'
          - '44'
        encf:
          type: string
          pattern: ^E[0-9]{12}$
          example: E310000000001
        client_request_id:
          type: string
          minLength: 8
          maxLength: 64
          description: Body-level idempotency key (per-tenant). The HTTP `Idempotency-Key` header covers
            the network retry layer.
        company:
          $ref: '#/components/schemas/InvoiceParty'
        customer:
          $ref: '#/components/schemas/InvoiceParty'
        parent_encf:
          type: string
          nullable: true
          description: Required for credit/debit notes (types 33, 34).
        lines:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/InvoiceLine'
        totals:
          $ref: '#/components/schemas/InvoiceTotals'
    InvoiceAccepted:
      type: object
      required:
      - docId
      - status
      properties:
        docId:
          type: string
          example: encf-1a2b3c4d
        trackId:
          type: string
          nullable: true
        status:
          type: string
          enum:
          - accepted
          - processing
          - replay
        encf:
          type: string
          example: E310000000001
        receivedAt:
          type: string
          format: date-time
    InvoiceStatus:
      type: object
      required:
      - docId
      - status
      properties:
        docId:
          type: string
        status:
          type: string
          enum:
          - pending
          - processing
          - accepted
          - rejected
          - stuck
        dgiiTrackId:
          type: string
          nullable: true
        dgiiResponseAt:
          type: string
          format: date-time
          nullable: true
        rejectionReason:
          type: string
          nullable: true
    NotifyByEmailRequest:
      type: object
      required:
      - docId
      - recipientEmail
      properties:
        docId:
          type: string
        recipientEmail:
          type: string
          format: email
        cc:
          type: array
          items:
            type: string
            format: email
    NotificationAccepted:
      type: object
      required:
      - notificationId
      properties:
        notificationId:
          type: string
        queuedAt:
          type: string
          format: date-time
    ReceivedDocument:
      type: object
      required:
      - id
      - issuerRnc
      - encf
      - receivedAt
      properties:
        id:
          type: string
        issuerRnc:
          type: string
          pattern: ^[0-9]{9,11}$
        encf:
          type: string
          pattern: ^E[0-9]{12}$
        ecfType:
          type: string
        receivedAt:
          type: string
          format: date-time
        xmlBase64:
          type: string
          format: byte
          nullable: true
    ReceivedDocumentsPage:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ReceivedDocument'
        nextCursor:
          type: string
          nullable: true
    ReceiveAccepted:
      type: object
      required:
      - id
      properties:
        id:
          type: string
        receivedAt:
          type: string
          format: date-time
    CommercialApproval:
      type: object
      required:
      - docId
      - status
      properties:
        docId:
          type: string
        status:
          type: string
          enum:
          - pending
          - approved
          - rejected
        decidedAt:
          type: string
          format: date-time
          nullable: true
    EmittedDocumentsReport:
      type: object
      required:
      - tenantId
      - totalEmitted
      properties:
        tenantId:
          type: string
        totalEmitted:
          type: integer
          minimum: 0
        byType:
          type: object
          additionalProperties:
            type: integer
            minimum: 0
        from:
          type: string
          format: date
          nullable: true
        to:
          type: string
          format: date
          nullable: true
    DgiiStatus:
      type: object
      required:
      - tenantId
      - dgiiOperational
      properties:
        tenantId:
          type: string
        dgiiOperational:
          type: boolean
        message:
          type: string
          nullable: true
        nextMaintenanceWindow:
          type: object
          nullable: true
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
        polledAt:
          type: string
          format: date-time
    RegistryRow:
      type: object
      required:
      - rnc
      - name
      - status
      properties:
        rnc:
          type: string
          description: 11-digit zero-padded RNC.
        name:
          type: string
          description: Legal name (razón social).
        trade_name:
          type: string
          description: Trade name (nombre comercial).
          nullable: true
        economic_activity:
          type: string
          description: Economic activity classification.
          nullable: true
        status:
          type: string
          description: DGII taxpayer status (ACTIVO, SUSPENDIDO, DADO DE BAJA, etc.).
        payment_regime:
          type: string
          description: Payment regime (ORDINARIO, SIMPLIFICADO, etc.).
          nullable: true
    SearchResult:
      type: object
      required:
      - items
      - count
      - partial
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RegistryRow'
        count:
          type: integer
          minimum: 0
          description: Number of results in this response.
        partial:
          type: boolean
          description: True if more results exist beyond the limit.
    RegistryStatus:
      type: object
      required:
      - last_ingest
      - record_count
      - stale
      properties:
        last_ingest:
          type: string
          format: date-time
          description: Timestamp of the last successful ingest run.
        record_count:
          type: integer
          minimum: 0
          description: Total records in the registry.
        source_etag:
          type: string
          description: ETag of the DGII CSV source file.
          nullable: true
        source_sha256:
          type: string
          description: SHA-256 hash of the downloaded source file.
          nullable: true
        age_seconds:
          type: integer
          minimum: 0
          description: Seconds since the last successful ingest.
        stale:
          type: boolean
          description: True if age_seconds exceeds 129600 (36 hours).
