openapi: 3.1.0
info:
  title: Risiti Direct eTIMS API
  version: 1.0.0
  description: |
    Issue compliant KRA eTIMS records for the single verified business attached to your Direct API workspace.

    ## 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. **Use your verified business.** Direct API automatically applies the business verified during workspace onboarding; no merchant creation or `merchant_id` is required.
    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

    Direct API keys are permanently scoped to the one verified workspace business. Omit `merchant_id`; Risiti resolves it from the key and rejects cross-business access. 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 |
    | --- | --- | --- |
    | 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/direct
    description: Sandbox
  - url: https://api.getrisiti.com/v1/direct
    description: Production
security:
  - ApiKey: []
tags:
  - 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: 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:
      - KRA taxpayer lookup
      - KRA code reference
  - name: Issue compliant sales
    tags:
      - 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"
  /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
