> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qanapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Encrypt a payload

> Requires the `encrypt` action on this configuration, and a current
license: protecting new data is the one operation a lapsed license
refuses.

Two modes, chosen by header:

- **Field mode**, the default. `X-Qanapi-Fields` names dot-separated
  paths inside a JSON body. Each named leaf is replaced by an envelope
  string; everything else is returned untouched. A path that matches
  nothing is skipped, unless `X-Qanapi-Require-Fields` is set.
- **Whole-body mode**, selected by `X-Qanapi-Full-Body` or
  `X-Qanapi-Mode`. The whole body is treated as one payload and a single
  envelope is returned as `text/plain`, whatever the body was.

Every payload is AES-256-GCM under a fresh key encryption key and data
encryption key, so a nonce is never reused under a key.

Setting `X-Qanapi-Destination` turns the call into a proxy: the
processed payload is sent on to that URL and the answer is relayed back.




## OpenAPI

````yaml /openapi/stand-alone/v3.yaml post /api/v3/encryption/{proxy}/encrypt
openapi: 3.1.0
info:
  title: Qanapi Standalone API
  version: 3.0.0
  summary: Single-tenant encryption, key management, classification and audit service.
  description: >
    A standalone cryptographic service. Callers hand it data and it returns

    ciphertext, or hand it ciphertext and it returns data, with every operation

    authorised by policy and recorded on a tamper-evident audit trail.


    ## Authentication


    Two credentials reach the same principal:


    - **API key** in `X-Qanapi-Authorization`, for machine callers. A key
    reaches
      only the configurations it is linked to.
    - **User bearer token** in `Authorization`, from `POST /api/v3/auth/login`,
      for a person working through a console.

    An API key outranks a bearer token when a request carries both, because a

    key names a narrower credential than the user that owns it.


    ## Request bodies


    Endpoints that take a JSON body require `Content-Type: application/json`.

    The two encryption endpoints are the exception: in whole-body mode they

    accept any bytes at all, whatever the content type says.


    ## Errors


    Every error body carries `message`, which is human readable, and `error`, a

    stable machine readable code that clients can branch on. Validation failures

    additionally carry an `errors` object keyed by the field each message

    concerns; a failure with no single field to blame is filed under `request`.


    ## Rate limiting


    Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. The

    window is per node and keyed on the client address, so a cluster of n nodes

    admits n times the configured figure.


    `POST /api/v3/auth/login` is limited separately and more strictly, ten a

    minute against sixty, because it needs no credential to reach and each

    attempt costs a full password verification. A successful login clears that

    budget, so the stricter limit falls on failed attempts rather than on

    legitimate sign-ins.


    ## Correlation


    Send `X-Request-Id` to tie a request to your own trace. The value is

    preserved on the audit record and echoed on the response, provided it is at

    most 64 characters of letters, digits, `-`, `_`, `.` or `:`; anything else

    is replaced with a generated id. Omit the header and one is generated for

    you.


    ## End-to-end encrypted requests and responses


    Any endpoint accepts a request body wrapped as an RFC 7516 JWE, so the

    payload is unreadable by anything between your client and this service,

    including the reverse proxy that terminates TLS. Wrap the JSON body to the

    key from `GET /api/v3/qanapi-public-key` and send it with

    `Content-Type: application/jose`; `application/jwt` and

    `application/jose+json` are accepted too, and a body that is plainly a

    compact JWE is recognised even without one of those content types. Where the

    installation requires it, the wrapped payload must carry `iat`, `exp` and

    `jti` claims, which also make each payload single-use.


    To have the **response** wrapped in turn, send

    `X-Qanapi-Wrap-With-Key-Id` naming an RSA key held in this service's KMS

    that you hold the private half of. The response then arrives as a compact

    JWE with `Content-Type: application/jose`. The header spelling

    `wrap_with_key_id` is accepted as well. The wrapping key is resolved with

    the same permission a plain key read needs, so a caller who may not read

    that key gets an error instead of a wrapped body.
  license:
    name: Proprietary
    identifier: LicenseRef-Qanapi-Commercial
servers:
  - url: https://{host}
    description: A deployed instance, behind the reverse proxy that terminates TLS.
    variables:
      host:
        default: qanapi.example.com
  - url: http://localhost:8000
    description: A local cluster, through its load balancer.
security:
  - ApiKeyAuth: []
  - BearerAuth: []
tags:
  - name: STAND Health
    description: >
      Liveness and readiness probes. Registered outside the authentication,
      audit

      and transport layers, so a load balancer polling them without credentials

      still gets an answer.
  - name: STAND Authentication
    description: >
      Logging in. There is no registration endpoint: the first administrator is

      created from the command line on the host, so nothing reachable over the

      network can claim an installation that has no users yet. Later accounts
      are

      created through `POST /api/v3/users`.
  - name: STAND Users
    description: User accounts and their roles.
  - name: STAND API keys
    description: >
      Machine credentials. A key is scoped to a set of configurations and
      reaches

      nothing until it is linked to at least one.
  - name: STAND Configurations
    description: >
      Encryption configurations. A configuration owns a master key and is the
      unit

      of cryptographic isolation.
  - name: STAND Classifications
    description: Sensitivity labels and the clearance that reaches them.
  - name: STAND Policies
    description: Allow and deny rules binding a principal to actions on resources.
  - name: STAND Encryption
    description: >
      The data plane. Field-level or whole-body encryption under a
      configuration,

      optionally forwarded onward to a destination of the caller's choosing.
  - name: STAND KMS
    description: Customer-managed key lifecycle, import and export.
  - name: STAND Audit
    description: The tamper-evident record of everything the service did.
  - name: STAND Transport
    description: The service's own key pair, for end-to-end encrypted request bodies.
paths:
  /api/v3/encryption/{proxy}/encrypt:
    parameters:
      - $ref: '#/components/parameters/ProxySlug'
    post:
      tags:
        - STAND Encryption
      summary: Encrypt a payload
      description: |
        Requires the `encrypt` action on this configuration, and a current
        license: protecting new data is the one operation a lapsed license
        refuses.

        Two modes, chosen by header:

        - **Field mode**, the default. `X-Qanapi-Fields` names dot-separated
          paths inside a JSON body. Each named leaf is replaced by an envelope
          string; everything else is returned untouched. A path that matches
          nothing is skipped, unless `X-Qanapi-Require-Fields` is set.
        - **Whole-body mode**, selected by `X-Qanapi-Full-Body` or
          `X-Qanapi-Mode`. The whole body is treated as one payload and a single
          envelope is returned as `text/plain`, whatever the body was.

        Every payload is AES-256-GCM under a fresh key encryption key and data
        encryption key, so a nonce is never reused under a key.

        Setting `X-Qanapi-Destination` turns the call into a proxy: the
        processed payload is sent on to that URL and the answer is relayed back.
      operationId: encrypt
      parameters:
        - $ref: '#/components/parameters/Fields'
        - $ref: '#/components/parameters/FullBody'
        - $ref: '#/components/parameters/Mode'
        - $ref: '#/components/parameters/ClassificationHeader'
        - $ref: '#/components/parameters/RequireFields'
        - $ref: '#/components/parameters/Destination'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                Any JSON document in field mode. In whole-body mode the body may
                be any bytes at all, including non-JSON.
              additionalProperties: true
            example:
              name: Jane Doe
              ssn: 123-45-6789
              dob: '1980-01-01'
          text/plain:
            schema:
              type: string
            example: 123-45-6789
          application/octet-stream:
            schema:
              type: string
              format: binary
            example: 123-45-6789
      responses:
        '200':
          description: |
            In field mode, the document with the named fields replaced by
            envelopes. In whole-body mode, one envelope as `text/plain`.

            When `X-Qanapi-Destination` was set this is instead whatever the
            destination answered, relayed with its own status, body and headers,
            and stamped with `X-Qanapi-Forwarded-To`.
          headers:
            X-Qanapi-Action-Count:
              $ref: '#/components/headers/ActionCount'
            X-Qanapi-Forwarded-To:
              $ref: '#/components/headers/ForwardedTo'
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                name: Jane Doe
                ssn: QANAPI-RS:v1:MDFhMDg2ZGQ:Vpp6gZK4ZRUwgvpcnA:$
                dob: QANAPI-RS:v1:MDFhMDg2ZGU:T3RoZXJjaXBoZXJ0ZXh0:$
            text/plain:
              schema:
                type: string
              example: QANAPI-RS:v1:MDFhMDg2ZGQ:Vpp6gZK4ZRUwgvpcnA:$
        '400':
          $ref: '#/components/responses/NoCredential'
        '401':
          $ref: '#/components/responses/BadCredential'
        '403':
          description: |
            The policy engine refused the action, the caller does not hold the
            clearance named in `X-Qanapi-Classification`, or the license has
            lapsed. Applying a classification the caller lacks is refused rather
            than allowed, because it would let them write data they immediately
            cannot read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            No configuration has that proxy slug, the presented key is not
            linked to the one that does, or `X-Qanapi-Classification` names a
            classification that does not exist. The first two are deliberately
            not distinguished, so an unrelated key cannot enumerate
            configurations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          description: |
            The body is not the JSON that field mode requires, a field path
            matched nothing while `X-Qanapi-Require-Fields` was set and nothing
            was changed, or `X-Qanapi-Destination` is not an absolute `http` or
            `https` URL that this installation permits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          description: |
            A forwarding destination was named and could not be reached. The
            cryptography succeeded; the forward did not.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    ProxySlug:
      name: proxy
      in: path
      required: true
      description: |
        The configuration's proxy slug. Sixteen random lowercase letters and
        digits unless one was pinned at creation.
      schema:
        type: string
        examples:
          - 952zpx6vyig3sdyp
    Fields:
      name: X-Qanapi-Fields
      in: header
      description: |
        Comma-separated dot paths to encrypt or decrypt, for example
        `ssn,user.address.postcode`. Absent in whole-body mode. A path that
        matches nothing is skipped unless `X-Qanapi-Require-Fields` is set.

        A numeric segment indexes one element of an array, so `items.0.card`
        reaches exactly that element. A segment that is not a number, applied to
        an array, is mapped over every element instead: `items.card` reaches the
        `card` of each object in `items`, and each one counts separately towards
        `X-Qanapi-Action-Count`.

        Naming an array itself encrypts it whole, as one value, rather than
        element by element.
      schema:
        type: string
        examples:
          - ssn,user.dob,items.card
    FullBody:
      name: X-Qanapi-Full-Body
      in: header
      description: |
        Treat the whole body as one payload. Any of `true`, `1`, `yes` or `on`
        turns it on; anything else, or the header's absence, leaves field mode
        in effect.
      schema:
        type: string
        examples:
          - 'true'
    Mode:
      name: X-Qanapi-Mode
      in: header
      description: |
        Alternative spelling of whole-body mode. Either spelling of the value
        selects it; any other value leaves field mode in effect.
      schema:
        type: string
        enum:
          - full_body
          - full-body
    ClassificationHeader:
      name: X-Qanapi-Classification
      in: header
      description: |
        Apply a classification to what is being encrypted, by slug or id.
        Refused unless the caller holds that clearance. Decryption reads the
        classification from the record, so this header has no effect there.
      schema:
        type: string
        examples:
          - top-secret
    RequireFields:
      name: X-Qanapi-Require-Fields
      in: header
      description: |
        Fail the request when a named field path matched nothing, rather than
        returning the document unchanged. Off by default, and turned on by any
        of `true`, `1`, `yes` or `on`.

        Worth setting in any caller that would rather hear about a typo than
        store plaintext: without it, a mistyped path returns the original body
        with a success status, and only `X-Qanapi-Action-Count: 0` tells the two
        apart. Nothing is changed when the request fails this way.
      schema:
        type: string
        examples:
          - 'true'
    Destination:
      name: X-Qanapi-Destination
      in: header
      description: |
        Forward the processed payload to this absolute URL and relay the answer
        back, with its own status, body and headers. Must be `http` or `https`.
        Every request header travels except `x-qanapi-*`, `authorization` and
        the hop-by-hop set. Restricted by an allowlist where the installation
        sets one.
      schema:
        type: string
        format: uri
        examples:
          - https://payments.internal/charge
  headers:
    ActionCount:
      description: |
        How many cryptographic operations the request performed. A zero in field
        mode means no named path matched anything.
      schema:
        type: integer
    ForwardedTo:
      description: |
        Present only when the response was relayed from a destination, naming
        it, so a caller can tell a proxied answer from one this service
        produced.
      schema:
        type: string
        format: uri
    RateLimitLimit:
      description: Requests allowed per minute, per node.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the current window.
      schema:
        type: integer
  responses:
    NoCredential:
      description: |
        No credential was presented. Send an API key in
        `X-Qanapi-Authorization`, or a bearer token in `Authorization`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadCredential:
      description: A credential was presented and did not verify.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PayloadTooLarge:
      description: |
        The body exceeded the installation's ceiling, 10 MB by default.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: The rate limit for this client address was exhausted.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required:
        - message
        - error
      properties:
        message:
          type: string
          description: |
            Human readable. A server-side fault is described by its category and
            nothing more: operational detail stays in the service's own log.
          examples:
            - Unauthorized.
        error:
          type: string
          description: |
            A stable machine readable code. Branch on this rather than on
            `message`, which is shared between conditions that this code tells
            apart.
          examples:
            - InvalidApiKeyException
    ValidationError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required:
            - errors
          properties:
            errors:
              type: object
              description: >
                Messages keyed by the field they concern. A failure with no
                single

                field to blame is filed under `request`.
              additionalProperties:
                type: array
                items:
                  type: string
              examples:
                - type:
                    - The type field is required.
                  configuration_ids:
                    - No configuration has the id `...`.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Qanapi-Authorization
      description: |
        A machine credential, `qk_` followed by its secret. Only a hash of the
        secret is stored, so a key is displayed exactly once, when it is created
        or rotated. A key reaches only the configurations it is linked to.

        It may also be sent as `Authorization: Bearer qk_...`, which is
        recognised by the prefix.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        A user token from `POST /api/v3/auth/login`. The token asserts a role,
        but the database decides it, so a role changed or a user deleted after
        the token was issued takes effect immediately. A token minted before a
        password change is refused.

````