openapi: 3.1.0
info:
  title: Risiti eTIMS API
  version: 1.0.0
  description: |
    Integrate KRA eTIMS once, then issue compliant records for every business your product serves.

    ## Start in sandbox

    1. **Create a workspace and sandbox key** at [dashboard.getrisiti.com](https://dashboard.getrisiti.com/?mode=signup). Keep the key on your server; never ship it in a browser or mobile bundle.
    2. **Create a merchant.** Store the returned `merchant_id` against the business in your system. Sandbox creation does not require a live KRA connection.
    3. **Resolve KRA codes.** Use `GET /reference/kra-codes` for item classifications, tax types, quantity units, package units and item types. Save the selected codes with your own catalog records.
    4. **Register an item or service.** Use a stable `external_id` and an `Idempotency-Key` for this replay-protected creation operation.
    5. **Create an invoice.** A `202` response means Risiti durably accepted the record. Keep its Risiti id and follow `data.submission` or signed webhooks until KRA processing completes.
    6. **Plan the remaining compliance flows** your product needs: credit notes, purchases, stock movements or buyer-initiated invoices. Check the implementation limitations below before relying on a documented operation.
    7. **Request live access** only after the complete sandbox workflow works. Connect each merchant to KRA eTIMS before that merchant's first live submission.

    ## Known implementation limitations — 16 September 2026

    Source review identified HTTP namespace-parsing defects. Treat Platform merchant detail, compliance and eTIMS initialization, plus item detail and buyer-invoice detail/consent in both products, as unavailable pending namespace parser repair. Their paths and schemas below preserve the intended contract, not an executable workflow or a successful HTTP test. Affected operations carry `x-implementation-status: unavailable_pending_namespace_parser_repair`.

    This finding does not apply to the collection create/list handlers or the invoice and credit-note detail handlers; it is not an end-to-end availability claim for those other operations. Use supported collection listings for record reconciliation and contact support for merchant readiness. Buyer-invoice consent and audit-detail testing cannot be completed through the affected public routes. The public webhook registration API also does not accept `buyer_invoice.*` subscriptions; internal event support is not public subscription availability. No live KRA calls were exercised for this review.

    ## Request rules

    - Send `Authorization: Bearer rsk_test_...` in sandbox and `Authorization: Bearer rsk_live_...` in production.
    - Send `Content-Type: application/json` for JSON writes. `Idempotency-Key` replay protection applies to creation of invoices, credit notes, items, suppliers, purchases, stock movements and buyer invoices; those operations declare the required header below.
    - For those replay-protected operations, retain a stable key and unchanged JSON across retries; changed JSON with the same key returns a conflict. Store your own stable `external_id` and the Risiti resource id.
    - Merchant creation, webhook endpoint creation, webhook test delivery, merchant initialization and buyer-consent updates do not use this header-based replay store. Do not assume that sending a key makes them replay-safe. After an uncertain response, reconcile the existing resource through its list/GET endpoint or dashboard before repeating a setup action; repeating webhook creation or its test can create duplicate endpoints or deliveries.
    - Log the response `request_id`. It is the fastest way for Risiti support to trace one request without exposing customer data.
    - Amounts are tax-inclusive KES values unless a schema says otherwise. Dates use ISO 8601.

    ## Choose the right key model

    Platform keys manage many merchants and must send `merchant_id` where a schema requires it. Merchant-scoped keys are limited to one business, so Risiti can infer the merchant on supported requests. Start with a sandbox key. Live keys are issued only after workspace review and cannot be used against the sandbox server.

    | Workflow | Read scope | Write scope |
    | --- | --- | --- |
    | Merchants | `merchants:read` | `merchants:write` |
    | KRA taxpayer lookup | `taxpayers:read` | — |
    | Items and KRA codes | `items:read` | `items:write` |
    | Sales invoices | `invoices:read` | `invoices:write` |
    | Credit notes | `credit_notes:read` | `credit_notes:write` |
    | Suppliers and purchases | `suppliers:read`, `purchases:read` | `suppliers:write`, `purchases:write` |
    | Stock | `stock:read` | `stock:write` |
    | Buyer-initiated invoices | `buyer_invoices:read` | `buyer_invoices:write` |
    | Webhooks and logs | `webhooks:read`, `logs:read` | `webhooks:write` |

    ## KRA fields you must resolve

    | Request field | Reference family | Notes |
    | --- | --- | --- |
    | `item_class_code` | `item_class` | Ten-digit KRA classification; search by product or service name. |
    | `tax_type` | `tax_type` | `A` exempt, `B` 16% VAT, `C` zero-rated, `D` non-VAT; `E` is a legacy code, not a current 8% VAT option. |
    | `quantity_unit_code` | `quantity_unit` | Unit used to measure the item, such as pieces, kilograms or litres. |
    | `package_unit_code` | `package_unit` | Packaging presented to KRA; use the code returned by the reference endpoint. |
    | `item_type_code` | `item_type` | Raw material, finished product or service. |
    | `payment_method` | Schema enum | Use the values documented on `PaymentMethod`; `06` is mobile money. |

    Fetch reference data at setup time, cache it in your product and refresh it periodically. Do not invent KRA codes or infer tax treatment from an item name.

    **Legacy tax code E:** KRA states that the former 8% petroleum VAT rate was deleted from 1 July 2023. The enum retains `E` for compatibility with historical records; its presence is not permission to charge 8% on a current sale. Confirm the applicable tax treatment for the supply and transaction date before live use. [KRA VAT guidance](https://www.kra.go.ke/individual/filing-paying/types-of-taxes/value-added-tax), source checked 16 September 2026. This documentation clarification does not change API enum values or tax-engine behavior.

    ## KRA availability

    KRA-backed writes are saved by Risiti before external submission. If KRA is temporarily unavailable, Risiti reports a queued or waiting state and retries automatically. Each resource receives its own exponential-backoff schedule with jitter; the recovery sweep is only a safety net and spreads due work across a recovery window. Do not create a duplicate request. Validation or authorization failures require you to correct the request and submit it with a new idempotency key.

    ## One submission lifecycle

    `data.submission` is the canonical KRA-delivery lifecycle for invoices, credit notes, items, purchases and stock movements. Build completion checks with `submission.terminal` and `submission.succeeded`:

    | `submission.state` | Terminal | Succeeded | Client action |
    | --- | --- | --- | --- |
    | `queued` | No | No | None; Risiti owns delivery. |
    | `submitting_to_kra` | No | No | None; do not duplicate the request. |
    | `waiting_for_kra` | No | No | None; Risiti will retry at `next_retry_at`. |
    | `completed` | Yes | Yes | Store the returned KRA receipt fields. |
    | `action_required` | Yes | No | Correct the safe message or contact support, then create a corrected request with a new idempotency key. |

    Legacy invoice fields `status` and `etims_status` remain in v1 for compatibility but are deprecated aliases. `latest_submission` is diagnostic attempt history, not a separate lifecycle. Integrations must not combine or prioritize those fields over `submission`.

    ## Pagination and reconciliation

    Every collection endpoint uses cursor pagination. Send `limit` and then pass the opaque `data.pagination.next_cursor` value as `cursor` until `data.pagination.has_more` is false. `created_from` and `created_to` accept ISO 8601 timestamps and make daily or monthly reconciliation deterministic. Never construct, decode or persist assumptions about cursor contents.

    ## Common errors

    Every error uses `{ error: { code, message, details }, request_id }`. `401` means the key is missing, invalid or revoked; `403` means the key lacks a scope or the workspace is not allowed to use the selected environment; `409` means an idempotency key or resource state conflicts; `422` identifies fields or business rules that need correction; and `429` includes a `Retry-After` header. Risiti never returns internal stack traces to API consumers.
  termsOfService: https://getrisiti.com/terms
  x-acceptable-use-policy: https://getrisiti.com/acceptable-use
  x-data-responsibility: API customers are responsible for ensuring every submitted compliance record reflects a genuine, authorized transaction and uses correct identities, classifications, amounts, references, declarations and consents. Misuse may result in request blocking, key revocation, account suspension or termination, and regulatory reporting where required or permitted by law.
  x-rate-limits:
    platform_safety_ceiling_reads_per_key_per_minute: 60000
    platform_safety_ceiling_writes_per_key_per_minute: 30000
    fairness_reads_per_merchant_per_minute: 3000
    fairness_writes_per_merchant_per_minute: 1200
    oversized_json_status: 413
  contact:
    name: Risiti API support
    email: hello@getrisiti.com
    url: https://getrisiti.com/etims-api-integration-kenya
externalDocs:
  description: Integration overview, sandbox workflow and production readiness
  url: https://getrisiti.com/etims-api-integration-kenya
x-risiti-roadmap:
  buyer_initiated_invoicing:
    status: sandbox_preview
    landing_page: https://getrisiti.com/buyer-initiated-etims-api-kenya
    contract_direction: The sandbox models a dedicated buyer-invoices resource with seller identity, procurement evidence, consent-aware states, creation idempotency and an audit trail. Public buyer-invoice webhook subscriptions are not currently accepted.
    availability: Partial sandbox preview with buyer_invoices scopes. Detail and consent routes are unavailable pending namespace parser repair, so the public end-to-end consent and audit workflow is not available. Live transport remains disabled until the buyer has the applicable KRA approval and Risiti has configured the approved reverse-invoicing route.
servers:
  - url: https://sandbox-api.getrisiti.com/v1/platform
    description: Sandbox
  - url: https://api.getrisiti.com/v1/platform
    description: Production
security:
  - ApiKey: []
tags:
  - name: Getting started
    description: |
      Create the first merchant in sandbox. This is the first request for platform integrations; merchant-scoped keys can skip it because their workspace already identifies one business.

      **Required scope:** `merchants:write`.
  - name: KRA code reference
    description: |
      Search the same synchronized KRA classifications and standard code lists used by Risiti validation. Query these values instead of hard-coding long item-class lists in your product.

      **Required scope:** `items:read`.
  - name: KRA taxpayer lookup
    description: |
      Verify a KRA PIN and retrieve the limited public taxpayer identity returned by KRA's eCitizen taxpayer checker. This identity lookup is available to both sandbox and production keys; it never submits fiscal data or enables production access.

      **Required scope:** `taxpayers:read`.
  - name: Merchant setup
    description: |
      Intended merchant detail, compliance readiness and initialization operations are currently unavailable pending namespace parser repair. Collection creation/listing are outside this defect. Initialization is required for live KRA submission, not for sandbox testing or immediately after workspace sign-up; contact support for readiness until the HTTP route is repaired and tested.

      **Required scopes:** `merchants:read` or `merchants:write`.
  - name: Item catalog
    description: |
      Register each product, service or raw material before using it in live invoices, purchases or stock movements. Resolve classification and unit codes through the KRA code reference endpoint.

      **Required scopes:** `items:read` or `items:write`.
  - name: Sales invoices
    description: |
      Create ordinary seller-issued invoices, then inspect their KRA processing and receipt data. A `202` response is a durable acceptance, not a reason to resend.

      **Required scopes:** `invoices:read` or `invoices:write`.
  - name: Credit notes
    description: |
      Correct an accepted sales invoice. Always reference the original Risiti invoice and send positive line values; Risiti produces the negative KRA credit-note amounts.

      **Required scopes:** `credit_notes:read` or `credit_notes:write`.
  - name: Suppliers
    description: |
      Store merchant-owned supplier identities for purchase and buyer-initiated invoicing workflows.

      **Required scopes:** `suppliers:read` or `suppliers:write`.
  - name: Purchases
    description: |
      Record supplier purchases with catalog, tax and payment details, then follow asynchronous KRA delivery.

      **Required scopes:** `purchases:read` or `purchases:write`.
  - name: Stock movements
    description: |
      Record stock in or out for registered products and preserve the resulting KRA submission state. Balance validation and the stock write occur in one serializable Convex mutation. Concurrent sales cannot both commit against the same stale balance; one transaction is retried against the committed value and is rejected if it would make stock negative.

      **Required scopes:** `stock:read` or `stock:write`.
  - name: Buyer-initiated invoices
    description: |
      Partial sandbox preview for eligible procurement workflows. The intended detail and consent operations are unavailable pending namespace parser repair; this is not an executable end-to-end consent or audit walkthrough. Public webhook registration does not accept buyer-invoice event subscriptions. Live use requires the applicable KRA approval.

      **Required scopes:** `buyer_invoices:read` or `buyer_invoices:write`.
  - name: Webhooks
    description: |
      Receive signed lifecycle events instead of polling every resource. Acknowledge quickly, verify signatures and process events idempotently. Platform endpoints may be scoped to one `merchant_id`, isolating a slow subscriber from other merchants. Risiti retries each delivery independently with exponential backoff and jitter; your endpoint should return a successful response only after it has durably accepted the event.

      **Required scopes:** `webhooks:read` or `webhooks:write`.
  - name: Request logs
    description: |
      Inspect recent API requests by `request_id` while troubleshooting an integration. Logs expose safe request metadata, not API-key secrets or internal stack traces.

      **Required scope:** `logs:read`.
x-tagGroups:
  - name: Start here
    tags: [Getting started, KRA taxpayer lookup, KRA code reference]
  - name: Issue compliant sales
    tags: [Merchant setup, Item catalog, Sales invoices, Credit notes]
  - name: Procurement and stock
    tags: [Suppliers, Purchases, Stock movements, Buyer-initiated invoices]
  - name: Operate reliably
    tags: [Webhooks, Request logs]
paths:
  /kra/pin-checker:
    get:
      tags: [KRA taxpayer lookup]
      summary: Verify and prefill a KRA taxpayer
      description: Available in sandbox and production. Risiti queries KRA's public eCitizen taxpayer checker and returns the registered identity for review. This lookup does not initialize eTIMS, submit fiscal data or change production access.
      parameters:
        - name: pin
          in: query
          required: true
          schema:
            type: string
            pattern: "^[AP][0-9]{9}[A-Z]$"
            example: P123456789Z
      responses:
        "200":
          description: KRA taxpayer identity verified
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [pin, legal_name, registrations]
                    properties:
                      pin: { type: string }
                      legal_name: { type: string }
                      trading_name: { type: string }
                      business_certificate_id: { type: string }
                      registrations:
                        type: object
                        properties:
                          vat: { type: boolean }
                          paye: { type: boolean }
                          turnover_tax: { type: boolean }
                          partnership: { type: boolean }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /merchants:
    post:
      tags: [Getting started]
      summary: Create a merchant
      description: Register a business that will issue compliant invoices through your platform. With `kra_pin` in either sandbox or production, Risiti verifies the PIN through KRA's public eCitizen taxpayer checker and prefills the registered legal name. If KRA is temporarily unavailable, supplied legal details are saved with verification pending. Fiscal submissions remain isolated to the key's environment. This setup operation does not implement Idempotency-Key replay protection. Keep the returned merchant id; after an uncertain response, inspect the merchant list or dashboard rather than blindly retrying. Merchant-slug uniqueness is not a stored response-replay guarantee.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMerchantRequest"
      responses:
        "201":
          description: Merchant created
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    $ref: "#/components/schemas/Merchant"
                  request_id:
                    type: string
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Merchant setup]
      summary: List merchants
      description: Returns businesses visible to the API key. Platform keys see their connected portfolio; merchant-scoped keys see only their assigned business. Keep the returned `merchant_id` as the stable Risiti identifier for later catalog and transaction requests.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
      responses:
        "200":
          description: Merchants
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [merchants]
                    properties:
                      merchants:
                        type: array
                        items:
                          $ref: "#/components/schemas/Merchant"
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                  request_id:
                    type: string
  /merchants/{merchantId}:
    get:
      tags: [Merchant setup]
      summary: Get a merchant
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair. The current handler reads the resource segment instead of merchantId. Intended behavior after repair is to return business identity, environment and onboarding state for one visible merchant. Use the merchant collection listing for reconciliation and contact support for readiness; the compliance detail route is also affected. This description is based on source review, not an exercised HTTP response."
      parameters:
        - $ref: "#/components/parameters/MerchantId"
      responses:
        "200":
          description: Merchant
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    $ref: "#/components/schemas/Merchant"
                  request_id:
                    type: string
        "404":
          $ref: "#/components/responses/NotFound"
  /merchants/{merchantId}/etims/initialize:
    post:
      tags: [Merchant setup]
      summary: Connect a merchant to KRA eTIMS
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair. The current handler checks the wrong action segments and cannot match the intended namespaced URL. Contact support for merchant readiness; do not treat this route as a completed onboarding step. Intended behavior after repair is live KRA initialization using a stable, merchant-specific device serial. Sandbox invoices do not require this step. This description is based on source review, not a live initialization test."
      parameters:
        - $ref: "#/components/parameters/MerchantId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [branch_id, device_serial]
              properties:
                branch_id:
                  type: string
                  pattern: "^[0-9]{2}$"
                  default: "00"
                device_serial:
                  type: string
                  minLength: 1
                  maxLength: 80
      responses:
        "200":
          description: Merchant eTIMS connection established
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [connected, mode]
                    properties:
                      connected:
                        type: boolean
                      mode:
                        type: string
                        enum: [test, live]
                      sdc_id:
                        type: [string, "null"]
                  request_id:
                    type: string
  /merchants/{merchantId}/compliance:
    get:
      tags: [Merchant setup]
      summary: Get merchant compliance health
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair. The current handler reads the wrong merchant and compliance-action segments. Intended behavior after repair is a merchant submission-health summary. Do not build readiness checks on this route yet; contact support. Invoice detail and supported invoice webhooks are outside this parser finding. This description is based on source review, not an exercised HTTP response."
      parameters:
        - $ref: "#/components/parameters/MerchantId"
      responses:
        "200":
          description: Compliance health
          content:
            application/json:
              schema:
                type: object
                required: [merchant_id, health_score, invoice_counts]
                properties:
                  merchant_id:
                    type: string
                  health_score:
                    type: integer
                    minimum: 0
                    maximum: 100
                  invoice_counts:
                    type: object
                    properties:
                      submitted:
                        type: integer
                      pending:
                        type: integer
                      failed:
                        type: integer
  /reference/kra-codes:
    get:
      tags: [KRA code reference]
      summary: Find KRA classifications and standard codes
      description: Use this endpoint while building catalog and transaction forms. `item_class` contains the ten-digit KRA classification used as `item_class_code`; the remaining families map directly to the corresponding request fields. Results come from Risiti's synchronized KRA reference store and may change when KRA publishes updates.
      parameters:
        - in: query
          name: type
          required: true
          description: Code family to return.
          schema:
            type: string
            enum: [item_class, tax_type, quantity_unit, package_unit, item_type]
        - in: query
          name: search
          description: Case-insensitive search by code or name. Use product or service words to search the large item-class list.
          schema:
            type: string
            minLength: 1
            maxLength: 100
          example: software
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Matching KRA codes
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [type, description, source, codes, pagination]
                    properties:
                      type:
                        type: string
                      description:
                        type: string
                      source:
                        type: string
                        enum: [kra_cache, bundled_reference]
                        description: "`kra_cache` for synchronized KRA data or `bundled_reference` for stable standard-code families."
                      codes:
                        type: array
                        items:
                          $ref: "#/components/schemas/KraReferenceCode"
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                  request_id:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          description: KRA item classifications have not been synchronized yet. Retry later and never substitute an unverified code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /invoices:
    post:
      tags: [Sales invoices]
      summary: Create and submit an invoice
      description: Accepts and durably saves an invoice for asynchronous eTIMS processing. A temporary KRA outage never requires the client to resubmit this request; inspect `data.submission` or consume signed webhooks for progress.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInvoiceRequest"
      responses:
        "202":
          description: Invoice accepted for processing
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    $ref: "#/components/schemas/InvoiceAccepted"
                  request_id:
                    type: string
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Sales invoices]
      summary: List invoices
      description: Returns invoices visible to the key, optionally narrowed by merchant, resource type or legacy storage state. Traverse the complete result set with `data.pagination.next_cursor`; use the individual invoice endpoint or signed webhooks when waiting for one KRA result. Use each returned invoice's canonical `submission.state`, `terminal` and `succeeded` fields for lifecycle decisions.
      parameters:
        - in: query
          name: merchant_id
          schema:
            type: string
        - in: query
          name: status
          deprecated: true
          description: Legacy invoice-storage filter. Prefer `submission_state`.
          schema:
            $ref: "#/components/schemas/InvoiceStatus"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - $ref: "#/components/parameters/SubmissionStateQuery"
        - $ref: "#/components/parameters/EtimsStatusQuery"
        - in: query
          name: external_id
          schema:
            type: string
          description: Exact caller-supplied invoice identifier.
        - in: query
          name: type
          schema:
            type: string
            enum: [sale, refund, credit_note]
      responses:
        "200":
          description: Invoices
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [invoices]
                    properties:
                      invoices:
                        type: array
                        items:
                          $ref: "#/components/schemas/Invoice"
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                  request_id: { type: string }
  /invoices/{invoiceId}:
    get:
      tags: [Sales invoices]
      summary: Get invoice status and receipt data
      description: Returns the durable invoice record, current submission lifecycle and any KRA receipt fields available after acceptance. Poll this resource only when webhooks are not practical, and stop polling once the state is terminal.
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: Invoice
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/Invoice" }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
  /credit-notes:
    post:
      tags: [Credit notes]
      summary: Issue a credit note against an accepted sales invoice
      description: Creates a KRA credit note with negative line and tax values and links it to the original accepted invoice. The original invoice must belong to the same merchant. Risiti safely retries temporary KRA failures without requiring another credit note request.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCreditNoteRequest"
      responses:
        "202":
          description: Credit note accepted for processing
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/InvoiceAccepted" }
                  request_id: { type: string }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Credit notes]
      summary: List credit notes
      description: Returns credit notes visible to the key and their current KRA processing state. Platform keys can narrow the result to one business with `merchant_id`.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - $ref: "#/components/parameters/EtimsStatusQuery"
      responses:
        "200":
          description: Credit notes visible to this API key
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [credit_notes]
                    properties:
                      credit_notes:
                        type: array
                        items: { $ref: "#/components/schemas/Invoice" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /credit-notes/{creditNoteId}:
    get:
      tags: [Credit notes]
      summary: Get a credit note and KRA submission status
      description: Returns the credit note, its original-invoice relationship and the latest KRA lifecycle data. A pending or retrying state means Risiti still owns delivery; do not issue a duplicate correction.
      parameters:
        - $ref: "#/components/parameters/CreditNoteId"
      responses:
        "200":
          description: Credit note
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/Invoice" }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
  /items:
    post:
      tags: [Item catalog]
      summary: Register an item or service
      description: Creates a merchant-scoped catalog item. Live keys queue the KRA registration; sandbox keys return a simulated acceptance. Temporary KRA failures are retried automatically and exposed through `data.submission`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateItemRequest"
      responses:
        "202":
          description: Item accepted for processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItemResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Item catalog]
      summary: List merchant items and KRA registration status
      description: Returns catalog records visible to the key, including the selected KRA classification, units, tax type and current registration state. Use `merchant_id` with platform keys.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - $ref: "#/components/parameters/EtimsStatusQuery"
      responses:
        "200":
          description: Items
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items: { $ref: "#/components/schemas/Item" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /items/{itemId}:
    get:
      tags: [Item catalog]
      summary: Get an item
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair in both Direct and Platform API. The current handler reads items instead of itemId. Intended behavior after repair is to return the merchant-owned catalog item and its KRA submission state. Reconcile through the item collection listing meanwhile. This description is based on source review, not an exercised HTTP response."
      parameters:
        - $ref: "#/components/parameters/ItemId"
      responses:
        "200":
          description: Item
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/Item" }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
  /suppliers:
    post:
      tags: [Suppliers]
      summary: Create a supplier
      description: Stores a merchant-owned supplier identity used by purchase and reverse-invoicing workflows.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSupplierRequest"
      responses:
        "201":
          description: Supplier created
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Suppliers]
      summary: List suppliers
      description: Returns supplier identities owned by the selected merchant. Platform keys should provide `merchant_id`; supplier records are never shared between businesses.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - in: query
          name: status
          schema:
            type: string
            enum: [active, inactive]
      responses:
        "200":
          description: Suppliers
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [suppliers, pagination]
                    properties:
                      suppliers:
                        type: array
                        items: { $ref: "#/components/schemas/Supplier" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /suppliers/{supplierId}:
    get:
      tags: [Suppliers]
      summary: Get a supplier
      description: Returns one merchant-owned supplier identity for purchase and buyer-initiated workflows.
      parameters:
        - $ref: "#/components/parameters/SupplierId"
      responses:
        "200":
          description: Supplier
        "404":
          $ref: "#/components/responses/NotFound"
  /purchases:
    post:
      tags: [Purchases]
      summary: Record and submit a supplier purchase
      description: Records a supplier invoice and its items. Live keys queue the purchase transaction for KRA after validating supplier, catalog ownership and eTIMS readiness. Risiti retries temporary KRA failures automatically.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePurchaseRequest"
      responses:
        "202":
          description: Purchase accepted for processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PurchaseResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Purchases]
      summary: List supplier purchases
      description: Returns purchase records and their asynchronous KRA states. Filter by `merchant_id` for a platform portfolio; use webhooks or the detail endpoint for submission progress.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - $ref: "#/components/parameters/EtimsStatusQuery"
      responses:
        "200":
          description: Purchases
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [purchases]
                    properties:
                      purchases:
                        type: array
                        items: { $ref: "#/components/schemas/Purchase" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /purchases/{purchaseId}:
    get:
      tags: [Purchases]
      summary: Get a supplier purchase and KRA status
      description: Returns one purchase, its line items and the latest KRA submission lifecycle. Pending and retrying purchases are already queued for delivery and must not be recreated.
      parameters:
        - $ref: "#/components/parameters/PurchaseId"
      responses:
        "200":
          description: Purchase
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/Purchase" }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
  /stock-movements:
    post:
      tags: [Stock movements]
      summary: Record a compliant stock movement
      description: Records stock in or out for a registered product. Risiti rejects movements that would make the merchant's stock negative, durably queues live movements, and retries temporary KRA failures without duplicating an already accepted stock stage.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateStockMovementRequest"
      responses:
        "202":
          description: Stock movement accepted for processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StockMovementResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Stock movements]
      summary: List stock movements and resulting balances
      description: Returns stock-in and stock-out records visible to the key, including the resulting balance and KRA processing state. Filter by merchant when operating a multi-business platform.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - $ref: "#/components/parameters/EtimsStatusQuery"
      responses:
        "200":
          description: Stock movements
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [stock_movements]
                    properties:
                      stock_movements:
                        type: array
                        items: { $ref: "#/components/schemas/StockMovement" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /stock-movements/{stockMovementId}:
    get:
      tags: [Stock movements]
      summary: Get a stock movement
      description: Returns one stock movement, its linked catalog item, resulting balance and latest KRA submission state.
      parameters:
        - $ref: "#/components/parameters/StockMovementId"
      responses:
        "200":
          description: Stock movement
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: "#/components/schemas/StockMovement" }
                  request_id: { type: string }
        "404":
          $ref: "#/components/responses/NotFound"
  /buyer-invoices:
    post:
      tags: [Buyer-initiated invoices]
      summary: Create a sandbox buyer-initiated invoice
      description: Creates a buyer invoice and starts a 30-day seller-consent window. A sandbox request is never represented as a live KRA submission.
      x-availability: sandbox
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBuyerInvoiceRequest"
      responses:
        "202":
          description: Buyer invoice accepted and awaiting sandbox seller consent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BuyerInvoiceResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Buyer-initiated invoices]
      summary: List buyer-initiated invoices
      description: Returns reverse-invoicing requests visible to the key, optionally filtered by merchant or consent/submission state. Live availability remains subject to the buyer's applicable KRA approval.
      parameters:
        - in: query
          name: merchant_id
          schema:
            type: string
        - in: query
          name: status
          schema:
            $ref: "#/components/schemas/BuyerInvoiceStatus"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
      responses:
        "200":
          description: Buyer invoices visible to this API key
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [buyer_invoices, pagination]
                    properties:
                      buyer_invoices:
                        type: array
                        items: { $ref: "#/components/schemas/BuyerInvoiceSummary" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /buyer-invoices/{buyerInvoiceId}:
    get:
      tags: [Buyer-initiated invoices]
      summary: Get a buyer invoice and immutable lifecycle events
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair in both Direct and Platform API. The current handler reads buyer-invoices instead of buyerInvoiceId. Intended behavior after repair is to return the buyer invoice and immutable audit events. Collection listing can reconcile saved records but is not a replacement for unavailable audit-detail inspection. This description is based on source review, not an exercised HTTP response."
      parameters:
        - $ref: "#/components/parameters/BuyerInvoiceId"
      responses:
        "200":
          description: Buyer invoice
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BuyerInvoiceResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /buyer-invoices/{buyerInvoiceId}/consent:
    post:
      tags: [Buyer-initiated invoices]
      summary: Simulate the seller's sandbox consent decision
      x-implementation-status: unavailable_pending_namespace_parser_repair
      description: "Known implementation limitation, 16 September 2026: unavailable pending namespace parser repair in both Direct and Platform API. The current handler checks the wrong consent-action segment and cannot match the intended namespaced URL. Intended sandbox-only behavior after repair is to record approval or rejection and queue simulated acceptance for approval. Do not claim an executable end-to-end consent walkthrough. Production consent requires the buyer's KRA-approved route. This description is based on source review, not an exercised HTTP response."
      x-availability: sandbox
      parameters:
        - $ref: "#/components/parameters/BuyerInvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [approved, rejected]
                reason:
                  type: string
                reference:
                  type: string
      responses:
        "202":
          description: Consent decision recorded
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
  /webhook-endpoints:
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint
      description: Registers a public HTTPS endpoint and returns its signing secret once. This setup operation does not implement Idempotency-Key replay protection; repeating it can create another endpoint. After an uncertain response inspect GET /webhook-endpoints or the dashboard before repeating registration; list responses do not recover the signing secret. For every delivery, compute HMAC-SHA256 over `{X-Risiti-Timestamp}.{raw_request_body}` and compare it with the `v1` value in `X-Risiti-Signature`. Also use `X-Risiti-Delivery` to make your event handler idempotent and reject stale timestamps.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookEndpointRequest"
      responses:
        "201":
          description: Webhook endpoint created
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
                  request_id: { type: string }
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      description: Returns registered endpoints and delivery configuration visible to this API key. Signing secrets are shown only when an endpoint is created and are never returned by this list.
      parameters:
        - $ref: "#/components/parameters/MerchantQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - in: query
          name: status
          schema:
            type: string
            enum: [active, disabled]
      responses:
        "200":
          description: Webhook endpoints
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [endpoints, pagination]
                    properties:
                      endpoints:
                        type: array
                        items:
                          $ref: "#/components/schemas/WebhookEndpoint"
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
  /webhook-endpoints/{endpointId}/test:
    post:
      tags: [Webhooks]
      summary: Send a test webhook event
      description: Queues a signed test delivery to the selected endpoint so you can verify signature checking, delivery deduplication and response handling before subscribing production traffic. This operation does not implement Idempotency-Key replay protection; repeating it queues another test. Inspect delivery history after an uncertain response before requesting a new test.
      parameters:
        - in: path
          name: endpointId
          required: true
          schema:
            type: string
      responses:
        "202":
          description: Test event queued
  /api-logs:
    get:
      tags: [Request logs]
      summary: List API request logs
      description: Returns recent request metadata for troubleshooting by `request_id`, route and response status. Logs intentionally omit full API keys, secrets, raw credentials and internal stack traces.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/CreatedFrom"
        - $ref: "#/components/parameters/CreatedTo"
        - in: query
          name: status_code
          schema:
            type: integer
            minimum: 100
            maximum: 599
      responses:
        "200":
          description: API logs
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [logs, pagination]
                    properties:
                      logs:
                        type: array
                        items:
                          $ref: "#/components/schemas/ApiLog"
                      pagination: { $ref: "#/components/schemas/Pagination" }
                  request_id: { type: string }
components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: Keep API keys secret and server-side. Use the minimum required scopes and revoke exposed or unused keys. Suspended workspaces and revoked keys are rejected server-side in both sandbox and production.
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 120
      description: |
        Required only on creation operations that explicitly declare this parameter: invoices, credit notes, items, suppliers, purchases, stock movements and buyer invoices. Their replay records are retained for 30 days. An exact replay returns the original status and body with `Idempotency-Replayed: true`; `request_id` identifies the new HTTP request. Reusing the key with different canonical JSON returns `409` with `error.code: idempotency_conflict`; recover the original request before deciding whether a corrected request and new key are needed. This guarantee does not apply to merchant or webhook setup, webhook tests, merchant initialization or buyer-consent updates. Keep your resource mapping beyond the replay window.
    MerchantId:
      in: path
      name: merchantId
      required: true
      schema:
        type: string
      description: Risiti merchant id.
    InvoiceId:
      in: path
      name: invoiceId
      required: true
      schema:
        type: string
    MerchantQuery:
      in: query
      name: merchant_id
      required: false
      schema:
        type: string
      description: Required for platform API keys and inferred for merchant-scoped keys.
    CreditNoteId:
      in: path
      name: creditNoteId
      required: true
      schema:
        type: string
    ItemId:
      in: path
      name: itemId
      required: true
      schema:
        type: string
    SupplierId:
      in: path
      name: supplierId
      required: true
      schema:
        type: string
    PurchaseId:
      in: path
      name: purchaseId
      required: true
      schema:
        type: string
    StockMovementId:
      in: path
      name: stockMovementId
      required: true
      schema:
        type: string
    BuyerInvoiceId:
      in: path
      name: buyerInvoiceId
      required: true
      schema:
        type: string
    Limit:
      in: query
      name: limit
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 100
      description: Number of records to return in this page. Use the returned cursor to continue; the limit is not a total-result cap.
    Cursor:
      in: query
      name: cursor
      schema:
        type: string
      description: Opaque `data.pagination.next_cursor` from the previous page. Do not decode or construct cursors.
    CreatedFrom:
      in: query
      name: created_from
      schema:
        type: string
        format: date-time
      description: Include records created at or after this ISO 8601 timestamp.
    CreatedTo:
      in: query
      name: created_to
      schema:
        type: string
        format: date-time
      description: Include records created at or before this ISO 8601 timestamp.
    EtimsStatusQuery:
      in: query
      name: etims_status
      deprecated: true
      schema:
        $ref: "#/components/schemas/EtimsSubmissionStatus"
      description: Deprecated storage-state filter. Prefer filtering terminal business logic with the returned canonical `submission` object.
    SubmissionStateQuery:
      in: query
      name: submission_state
      schema:
        type: string
        enum: [queued, submitting_to_kra, waiting_for_kra, completed, action_required]
      description: Canonical lifecycle filter. For example, combine `submission_state=waiting_for_kra` with `created_to` to find older requests that Risiti still owns and will retry.
  responses:
    BadRequest:
      description: Invalid request or unsupported query value
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: API key does not have the required scope
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: |
        Resource-state conflict or, on creation operations declaring IdempotencyKey, key reuse with different request content. Exact stored replays on those operations are not errors; they return the original success response with `Idempotency-Replayed: true`. Other operations do not gain replay protection from this response definition.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServiceUnavailable:
      description: Upstream KRA verification is temporarily unavailable
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    Pagination:
      type: object
      required: [has_more, next_cursor]
      properties:
        has_more:
          type: boolean
          description: True when another page is available.
        next_cursor:
          type: [string, "null"]
          description: Opaque continuation cursor. Pass it unchanged as `cursor`; null marks the final page.
    ErrorResponse:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: array
              items:
                type: object
        request_id:
          type: string
          description: Include this value when contacting Risiti support.
    KraReferenceCode:
      type: object
      required: [code, name, description]
      properties:
        code:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
    PaymentMethod:
      type: string
      description: KRA payment method code. Use `06` for M-Pesa and other mobile money payments.
      oneOf:
        - const: "01"
          title: Cash
          description: Cash payment.
        - const: "02"
          title: Credit
          description: Credit sale or pay-later arrangement.
        - const: "03"
          title: Cash/Credit
          description: Mixed cash and credit payment.
        - const: "04"
          title: Bank Check
          description: Cheque or bank check payment.
        - const: "05"
          title: Debit & Credit Card
          description: Card payment.
        - const: "06"
          title: Mobile Money
          description: M-Pesa or other mobile money payment.
        - const: "07"
          title: Other
          description: Payment method outside the listed KRA categories.
      x-enumDescriptions:
        "01": Cash
        "02": Credit
        "03": Cash/Credit
        "04": Bank Check
        "05": Debit & Credit Card
        "06": Mobile Money
        "07": Other
    TaxType:
      type: string
      description: KRA tax type code applied to an invoice line item. Choose the applicable current tax treatment, not merely a supported enum. E is a legacy compatibility code; KRA states that the former 8% petroleum VAT rate was deleted from 1 July 2023.
      oneOf:
        - const: A
          title: Exempt
          description: Exempt tax treatment.
        - const: B
          title: VAT 16%
          description: Standard 16% VAT.
        - const: C
          title: Zero-rated
          description: Zero-rated supply.
        - const: D
          title: Non-VAT / Not applicable
          description: Used where VAT is not applicable.
        - const: E
          title: Legacy E (former 8% rate)
          description: Historical compatibility code, not a current reduced-rate option. KRA states that the former 8% petroleum VAT rate was deleted from 1 July 2023. Confirm the supply and transaction date before live use.
      x-enumDescriptions:
        A: Exempt
        B: VAT 16%
        C: Zero-rated
        D: Non-VAT / Not applicable
        E: Legacy E (former 8% rate; not current VAT guidance)
    KraSubmission:
      type: object
      description: Canonical durable Risiti-to-KRA lifecycle. This is the only object an integration should use for completion checks. Keep the resource id and poll its GET endpoint or consume signed webhooks; never duplicate a request while automatic retry is true.
      required:
        [accepted_by_risiti, state, terminal, succeeded, will_retry_automatically, attempt_count, message]
      properties:
        accepted_by_risiti:
          type: boolean
          const: true
        state:
          type: string
          enum: [queued, submitting_to_kra, waiting_for_kra, completed, action_required]
        terminal:
          type: boolean
          description: Stop polling when true. This is true for both successful completion and requests that need client action.
        succeeded:
          type: boolean
          description: True only when KRA processing completed successfully. Use `terminal && succeeded` for the simple “is this done successfully?” check.
        will_retry_automatically:
          type: boolean
        attempt_count:
          type: integer
          minimum: 0
        last_attempt_at:
          type: [integer, "null"]
          format: int64
          description: Unix time in milliseconds.
        next_retry_at:
          type: [integer, "null"]
          format: int64
          description: Unix time in milliseconds. Null when no automatic retry is pending.
        message:
          type: string
          description: Safe, actionable status text. Internal stack traces and credentials are never returned.
    CreateMerchantRequest:
      type: object
      description: Creates a platform-owned merchant. In production, business_name may be omitted when kra_pin is supplied because Risiti prefills the KRA-registered name.
      required: [external_id]
      properties:
        external_id:
          type: string
          example: garage_001
        business_name:
          type: string
          example: Kilimani Auto Garage
        kra_pin:
          type: string
          example: P051234567R
        branch_id:
          type: string
          pattern: "^[0-9]{2}$"
          default: "00"
        phone:
          type: string
          example: "254712345678"
        business_registration_number:
          type: string
          example: BN-2026-001
        business_type:
          type: string
          example: garage
    Merchant:
      type: object
      required:
        [
          merchant_id,
          platform_id,
          legal_name,
          slug,
          status,
          etims_status,
          default_branch_id,
          etims_connection_ready,
          sandbox_invoice_submission_ready,
          live_invoice_submission_ready,
          required_action,
          etims_initialization_path,
          onboarding_url,
          created_at,
          updated_at,
        ]
      properties:
        merchant_id:
          type: string
        platform_id:
          type: string
        legal_name:
          type: string
        trading_name:
          type: string
        slug:
          type: string
        kra_pin:
          type: string
        business_type:
          type: string
        contact_email:
          type: string
          format: email
        contact_phone:
          type: string
        status:
          type: string
          enum: [pending, active, suspended, archived]
        etims_status:
          type: string
          enum: [not_started, pending, connected, disconnected, failed]
          description: Merchant-level KRA connection state. It does not block Risiti sandbox simulation.
        default_branch_id:
          type: string
          pattern: "^[0-9]{2}$"
        etims_connection_ready:
          type: boolean
          description: True only when an active live KRA credential exists for this merchant.
        sandbox_invoice_submission_ready:
          type: boolean
        live_invoice_submission_ready:
          type: boolean
          description: Merchant-level readiness. Platform production approval and a live API key remain separately enforced.
        required_action:
          type: [string, "null"]
          enum: [initialize_etims, contact_support, null]
        etims_initialization_path:
          type: string
        onboarding_url:
          type: [string, "null"]
          format: uri
          description: Null unless a merchant-specific, secure hosted onboarding session has been created.
        created_at:
          type: integer
          format: int64
        updated_at:
          type: integer
          format: int64
    CreateInvoiceRequest:
      type: object
      description: Generally available v1 request for an ordinary merchant sales invoice. merchant_id is required only for Platform API keys and is inferred from a Direct API key. Buyer-initiated fields described by the roadmap schemas are not accepted by this request yet.
      required: [external_id, payment_method, items]
      properties:
        merchant_id:
          type: string
          description: Required for Platform API keys. Omit for Direct API; the verified workspace business is used automatically.
        external_id:
          type: string
        sales_date:
          type: string
          format: date
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        buyer:
          type: object
          properties:
            name:
              type: string
            kra_pin:
              type: string
            phone:
              type: string
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/InvoiceItem"
    CreateCreditNoteRequest:
      type: object
      description: Issues a credit note against an accepted sales invoice. Send positive quantities and prices; Risiti produces the KRA-required negative credit-note values.
      required:
        [
          external_id,
          original_invoice_id,
          reason_code,
          payment_method,
          items,
        ]
      properties:
        merchant_id:
          type: string
          description: Required for platform API keys and inferred for merchant-scoped keys.
        external_id:
          type: string
          maxLength: 128
        original_invoice_id:
          type: string
          description: Risiti id of the accepted original sales invoice.
        reason_code:
          type: string
          description: KRA credit-note reason code.
          default: "01"
          pattern: "^[0-9]{2}$"
        issued_at:
          type: string
          format: date-time
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        buyer:
          type: object
          properties:
            name:
              type: string
            kra_pin:
              type: string
            phone:
              type: string
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/InvoiceItem"
    CreateItemRequest:
      type: object
      description: Registers a product, service or raw material in a merchant's eTIMS catalog.
      required:
        [
          external_id,
          item_code,
          item_class_code,
          name,
          item_type_code,
          tax_type,
          default_price,
        ]
      properties:
        merchant_id:
          type: string
          description: Required for platform API keys and inferred for merchant-scoped keys.
        external_id:
          type: string
          maxLength: 128
        item_code:
          type: string
          maxLength: 40
          example: ITEM-001
        item_class_code:
          type: string
          pattern: "^[0-9]{10}$"
          description: Ten-digit KRA item classification code.
        name:
          type: string
          maxLength: 200
        item_type_code:
          type: string
          description: KRA item type; 1 is raw material, 2 is product and 3 is service.
          enum: ["1", "2", "3"]
        package_unit_code:
          type: string
          default: NT
        quantity_unit_code:
          type: string
          default: U
        tax_type:
          $ref: "#/components/schemas/TaxType"
        default_price:
          type: number
          minimum: 0
          description: Tax-inclusive selling price in KES.
        barcode:
          type: string
          maxLength: 100
    Item:
      type: object
      required: [item_id, merchant_id, mode, external_id, item_code, name, etims_status, submission]
      properties:
        item_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        mode: { type: string, enum: [test, live] }
        external_id: { type: string }
        item_code: { type: string }
        item_class_code: { type: string }
        item_type_code: { type: string, enum: ["1", "2", "3"] }
        name: { type: string }
        tax_type: { $ref: "#/components/schemas/TaxType" }
        default_price: { type: number }
        etims_status:
          type: string
          enum: [queued, retrying, accepted, rejected, failed, action_required]
        kra_result_code: { type: string }
        kra_result_message: { type: string }
        submission: { $ref: "#/components/schemas/KraSubmission" }
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    ItemResponse:
      type: object
      required: [data, request_id]
      properties:
        data: { $ref: "#/components/schemas/Item" }
        request_id: { type: string }
    CreateSupplierRequest:
      type: object
      description: Creates a merchant-owned supplier for purchase and reverse-invoicing workflows.
      required: [external_id, name]
      properties:
        merchant_id:
          type: string
          description: Required for platform API keys and inferred for merchant-scoped keys.
        external_id:
          type: string
          maxLength: 128
        name:
          type: string
          maxLength: 200
        kra_pin:
          type: string
          pattern: "^[AP][0-9]{9}[A-Z]$"
          description: Required before a live supplier purchase can be submitted to KRA.
        branch_id:
          type: string
          pattern: "^[0-9]{2}$"
          default: "00"
        phone:
          type: string
        email:
          type: string
          format: email
    Supplier:
      type: object
      required: [supplier_id, merchant_id, external_id, name, status, created_at, updated_at]
      properties:
        supplier_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        external_id: { type: string }
        name: { type: string }
        kra_pin: { type: [string, "null"] }
        branch_id: { type: string, pattern: "^[0-9]{2}$" }
        phone: { type: [string, "null"] }
        email: { type: [string, "null"], format: email }
        status: { type: string, enum: [active, inactive] }
        compliance_status:
          type: string
          description: Supplier identity readiness for live KRA purchase or reverse-invoicing workflows.
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    PurchaseItem:
      type: object
      required:
        [name, item_code, item_class_code, quantity, unit_price, tax_type]
      properties:
        item_id:
          type: string
          description: Optional Risiti catalog item id. Live requests must reference an accepted live catalog item.
        item_code:
          type: string
        item_class_code:
          type: string
          pattern: "^[0-9]{10}$"
        name:
          type: string
        quantity:
          type: number
          exclusiveMinimum: 0
        unit_price:
          type: number
          minimum: 0
          description: Tax-inclusive unit price in KES.
        discount_rate:
          type: number
          minimum: 0
          maximum: 100
          default: 0
        tax_type:
          $ref: "#/components/schemas/TaxType"
        package_unit_code:
          type: string
          default: NT
        quantity_unit_code:
          type: string
          default: U
    CreatePurchaseRequest:
      type: object
      description: Records a supplier invoice and submits its purchase transaction to KRA in live mode.
      required:
        [
          external_id,
          supplier_id,
          supplier_invoice_number,
          purchase_date,
          payment_method,
          items,
        ]
      properties:
        merchant_id:
          type: string
          description: Required for platform API keys and inferred for merchant-scoped keys.
        external_id:
          type: string
          maxLength: 128
        supplier_id:
          type: string
          description: Risiti supplier id owned by this merchant.
        supplier_invoice_number:
          type: integer
          minimum: 1
          description: Numeric KRA supplier invoice number.
        purchase_date:
          type: string
          format: date
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        items:
          type: array
          minItems: 1
          maxItems: 200
          items:
            $ref: "#/components/schemas/PurchaseItem"
    Purchase:
      type: object
      required: [purchase_id, merchant_id, mode, external_id, supplier_id, etims_status, submission]
      properties:
        purchase_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        mode: { type: string, enum: [test, live] }
        supplier_id: { type: string }
        external_id: { type: string }
        supplier_invoice_number: { type: integer }
        purchase_number: { type: integer }
        purchase_date: { type: string, format: date }
        payment_method: { $ref: "#/components/schemas/PaymentMethod" }
        items:
          type: array
          items: { $ref: "#/components/schemas/PurchaseItem" }
        subtotal: { type: number }
        total_tax: { type: number }
        total_amount: { type: number }
        etims_status:
          type: string
          enum: [queued, retrying, accepted, rejected, failed, action_required]
        kra_result_code: { type: string }
        kra_result_message: { type: string }
        submission: { $ref: "#/components/schemas/KraSubmission" }
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    PurchaseResponse:
      type: object
      required: [data, request_id]
      properties:
        data: { $ref: "#/components/schemas/Purchase" }
        request_id: { type: string }
    CreateStockMovementRequest:
      type: object
      description: Records stock received, adjusted in, sold, consumed or adjusted out for a registered product.
      required: [external_id, item_id, movement_type, quantity]
      properties:
        merchant_id:
          type: string
          description: Required for platform API keys and inferred for merchant-scoped keys.
        external_id:
          type: string
          maxLength: 128
        item_id:
          type: string
          description: Risiti catalog item id owned by this merchant.
        movement_type:
          type: string
          enum:
            [
              opening_balance,
              purchase,
              sale,
              adjustment_in,
              adjustment_out,
              return_in,
              return_out,
            ]
        quantity:
          type: number
          exclusiveMinimum: 0
        occurred_at:
          type: string
          format: date-time
        reference:
          type: string
          maxLength: 120
        notes:
          type: string
          maxLength: 500
    StockMovement:
      type: object
      required: [stock_movement_id, merchant_id, mode, external_id, item_id, movement_type, quantity, resulting_quantity, etims_status, submission]
      properties:
        stock_movement_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        mode: { type: string, enum: [test, live] }
        external_id: { type: string }
        item_id: { type: string }
        item_code: { type: string }
        movement_number: { type: integer }
        movement_type:
          type: string
          enum: [opening_balance, purchase, sale, adjustment_in, adjustment_out, return_in, return_out]
        quantity: { type: number }
        resulting_quantity: { type: number }
        occurred_at: { type: string, format: date-time }
        reference: { type: string }
        notes: { type: string }
        etims_status:
          type: string
          enum: [queued, retrying, accepted, rejected, failed, action_required]
        kra_result_code: { type: string }
        kra_result_message: { type: string }
        submission: { $ref: "#/components/schemas/KraSubmission" }
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    StockMovementResponse:
      type: object
      required: [data, request_id]
      properties:
        data: { $ref: "#/components/schemas/StockMovement" }
        request_id: { type: string }
    CreateBuyerInvoiceRequest:
      type: object
      x-availability: sandbox
      required:
        [
          external_id,
          seller,
          procurement,
          consent,
          currency,
          payment_method,
          items,
        ]
      properties:
        merchant_id:
          type: string
          description: Required with a platform key; inferred from a merchant key.
        external_id:
          type: string
          maxLength: 128
        seller:
          type: object
          required: [kra_pin, name, phone]
          properties:
            external_id:
              type: string
              description: Stable supplier identifier from the procurement platform.
            kra_pin:
              type: string
              pattern: "^[AP][0-9]{9}[A-Z]$"
            name:
              type: string
            phone:
              type: string
              description: Seller contact registered for the applicable consent workflow.
        procurement:
          type: object
          required: [external_id, accepted_at]
          properties:
            external_id:
              type: string
              description: Stable purchase, delivery or settlement identifier.
            accepted_at:
              type: string
              format: date-time
            evidence_url:
              type: string
              format: uri
              description: Optional reference to platform-held delivery or acceptance evidence.
        consent:
          type: object
          required: [method]
          properties:
            method:
              type: string
              description: Sandbox requests must use sandbox. Live methods are reserved for an approved KRA route.
              enum: [sandbox, kra_ussd, kra_ecitizen, approved_system]
            agreement_version:
              type: string
            reference:
              type: string
              description: External reference for consent evidence when one is available.
        currency:
          type: string
          const: KES
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        items:
          type: array
          minItems: 1
          maxItems: 200
          items:
            $ref: "#/components/schemas/BuyerInvoiceItem"
    BuyerInvoiceItem:
      type: object
      required: [name, quantity, unit_price, tax_type]
      properties:
        external_id:
          type: string
        name:
          type: string
          maxLength: 200
        quantity:
          type: number
          exclusiveMinimum: 0
        unit_price:
          type: number
          minimum: 0
          description: Tax-inclusive unit price in KES.
        tax_type:
          type: string
          enum: [A, B, C, D, E]
        item_class_code:
          type: string
    BuyerInvoiceStatus:
      type: string
      enum:
        [
          awaiting_seller_consent,
          seller_rejected,
          consent_expired,
          queued,
          submitted,
          accepted,
          rejected,
          failed,
        ]
    BuyerInvoiceSummary:
      type: object
      required: [buyer_invoice_id, merchant_id, external_id, mode, simulation, status, seller, procurement, consent, currency, items, totals, created_at, updated_at]
      properties:
        buyer_invoice_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        external_id: { type: string }
        mode: { type: string, enum: [test, live] }
        simulation: { type: boolean }
        status: { $ref: "#/components/schemas/BuyerInvoiceStatus" }
        seller: { type: object }
        procurement: { type: object }
        consent: { type: object }
        currency: { type: string, const: KES }
        payment_method: { $ref: "#/components/schemas/PaymentMethod" }
        items:
          type: array
          items: { $ref: "#/components/schemas/BuyerInvoiceItem" }
        totals: { type: object }
        kra_receipt_number: { type: [string, "null"] }
        submitted_at: { type: [integer, "null"], format: int64 }
        completed_at: { type: [integer, "null"], format: int64 }
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    BuyerInvoiceResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          type: object
          required:
            [
              buyer_invoice_id,
              merchant_id,
              external_id,
              mode,
              simulation,
              status,
              seller,
              procurement,
              consent,
              currency,
              items,
              totals,
            ]
          properties:
            buyer_invoice_id:
              type: string
            merchant_id:
              type: string
            external_id:
              type: string
            mode:
              type: string
              enum: [test, live]
            simulation:
              type: boolean
            status:
              $ref: "#/components/schemas/BuyerInvoiceStatus"
            seller:
              type: object
            procurement:
              type: object
            consent:
              type: object
            currency:
              type: string
              const: KES
            items:
              type: array
              items:
                $ref: "#/components/schemas/BuyerInvoiceItem"
            totals:
              type: object
            events:
              type: array
              items:
                type: object
        request_id:
          type: string
    InvoiceItem:
      type: object
      required: [external_id, name, quantity, unit_price, tax_type]
      properties:
        external_id:
          type: string
        name:
          type: string
        quantity:
          type: number
          minimum: 0
        unit_price:
          type: number
          minimum: 0
        discount_rate:
          type: number
          minimum: 0
          maximum: 100
          default: 0
        tax_type:
          $ref: "#/components/schemas/TaxType"
        item_class_code:
          type: string
    InvoiceAccepted:
      type: object
      required: [invoice_id, status, etims_status, risiti_url, submission, created_at]
      properties:
        invoice_id: { type: string }
        external_id: { type: [string, "null"] }
        status:
          allOf: [{ $ref: "#/components/schemas/InvoiceStatus" }]
          deprecated: true
          description: Legacy invoice-storage status. Use `submission` for all processing decisions.
        etims_status:
          allOf: [{ $ref: "#/components/schemas/EtimsSubmissionStatus" }]
          deprecated: true
          description: Legacy eTIMS storage status. Use `submission` for all processing decisions.
        risiti_url: { type: string, format: uri }
        submission: { $ref: "#/components/schemas/KraSubmission" }
        created_at: { type: integer, format: int64 }
    InvoiceStatus:
      type: string
      deprecated: true
      description: Legacy invoice-storage state retained for v1 response compatibility. Do not use it as a completion lifecycle.
      enum: [received, validation_failed, validated, queued, retrying, submitted, accepted, rejected, cancelled, manual_review_required, action_required]
    EtimsSubmissionStatus:
      type: string
      deprecated: true
      description: Legacy eTIMS persistence state retained for v1 response compatibility. Use `submission.state`, `submission.terminal` and `submission.succeeded`.
      enum: [not_submitted, queued, retrying, submitted, accepted, rejected, pending, failed, action_required]
    Invoice:
      type: object
      required: [invoice_id, merchant_id, status, etims_status, type, currency, issued_at, items, totals, submission, risiti_url, created_at, updated_at]
      properties:
        invoice_id: { type: string }
        merchant_id: { type: string }
        platform_id: { type: string }
        external_id: { type: string }
        status:
          allOf: [{ $ref: "#/components/schemas/InvoiceStatus" }]
          deprecated: true
        etims_status:
          allOf: [{ $ref: "#/components/schemas/EtimsSubmissionStatus" }]
          deprecated: true
        type: { type: string, enum: [sale, refund, credit_note] }
        currency: { type: string, const: KES }
        issued_at: { type: string, format: date-time }
        customer: { type: object }
        items:
          type: array
          items: { $ref: "#/components/schemas/InvoiceItem" }
        totals: { type: object }
        original_invoice_id: { type: string }
        original_kra_invoice_number: { type: integer }
        reason_code: { type: string }
        kra_invoice_number: { type: integer }
        kra_receipt_number: { type: string }
        kra_receipt_signature: { type: string }
        kra_control_unit_datetime: { type: string }
        qr_code_data: { type: string }
        validation_errors:
          type: array
          items: { type: object }
        submission: { $ref: "#/components/schemas/KraSubmission" }
        latest_submission:
          type: [object, "null"]
          description: Diagnostic record for the most recent KRA attempt. It can briefly lag the canonical `submission` projection and must not drive completion logic.
          properties:
            submission_id: { type: string }
            authority: { type: string, const: KRA_ETIMS }
            status: { type: string }
            response: { type: object }
            error_code: { type: string }
            error_message: { type: string }
            next_retry_at: { type: integer, format: int64 }
        risiti_url: { type: string, format: uri }
        created_at: { type: integer, format: int64 }
        updated_at: { type: integer, format: int64 }
    CreateWebhookEndpointRequest:
      type: object
      description: Delivery bodies are signed as `t=<unix>,v1=<hex>` in `X-Risiti-Signature`, where the digest is HMAC-SHA256 over `<unix>.<raw-body>`. Use `X-Risiti-Delivery` for idempotent processing.
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - invoice.accepted
              - invoice.rejected
              - invoice.pending
              - invoice.retrying
              - invoice.failed
              - item.accepted
              - item.retrying
              - item.rejected
              - item.action_required
              - purchase.accepted
              - purchase.retrying
              - purchase.rejected
              - purchase.action_required
              - stock_movement.accepted
              - stock_movement.retrying
              - stock_movement.rejected
              - stock_movement.action_required
              - webhook.test
    WebhookEndpoint:
      type: object
      required: [id, url, events, status]
      properties:
        id:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [active, disabled]
    ApiLog:
      type: object
      properties:
        id:
          type: string
        request_id:
          type: string
        method:
          type: string
        path:
          type: string
        status_code:
          type: integer
        latency_ms:
          type: integer
        api_key_prefix:
          type: string
        idempotency_key:
          type: string
        error_code:
          type: string
        created_at:
          type: string
          format: date-time
