{
  "openapi": "3.1.0",
  "info": {
    "title": "K8 HQ Content API",
    "version": "1",
    "summary": "Read and send mail in the K8 HQ inboxes you have access to.",
    "description": "Two ways in, one API:\n\n- **API tokens** (scripts, AI agents, integrations) use `https://api.k8hq.com/v1` with `Authorization: Bearer k8hq_...`. A token belongs to one project, acts for a member with that member's current access, can be narrowed to some inboxes, and has scopes: `messages:read`, `content:read`, `flags:write`, `send`. K8 HQ staff issue tokens for now.\n\n- **The K8 HQ web app** uses `https://app.k8hq.com/api/v1` with a session from signing in with a passkey.\n\nMessage content is encrypted at rest. Metadata (sender, subject, flags) comes back in plain JSON. With `content:read`, a token can read a message decrypted (`/content`, `/source`, `/attachments/{n}`) from inboxes in compatible mode, where K8 HQ's servers can open them; each such read is recorded in the inbox's audit log. Zero-access inboxes, and the web app, get ciphertext only (`/raw`), opened with keys sealed to the user's own key (`/keys`).\n\nResponses that never change (`/raw`) may be kept by the browser; message metadata carries an ETag to revalidate. Shared caches never store responses.\n\nErrors are JSON objects with one field, `error`. Inboxes you have no access to, and messages or labels that aren't in the inbox, return 404."
  },
  "servers": [
    {
      "url": "https://api.k8hq.com/v1",
      "description": "API tokens"
    },
    {
      "url": "https://app.k8hq.com/api/v1",
      "description": "The K8 HQ web app (sessions)"
    }
  ],
  "security": [
    {
      "token": [
        "messages:read"
      ]
    },
    {
      "session": []
    }
  ],
  "tags": [
    {
      "name": "Inboxes"
    },
    {
      "name": "Messages"
    },
    {
      "name": "Content"
    },
    {
      "name": "Sending"
    },
    {
      "name": "Events"
    }
  ],
  "paths": {
    "/event-types": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEventTypes",
        "summary": "List event types",
        "description": "The event catalog: every event type K8 HQ records, with its JSON Schema. Audit-only types are listed with their visibility; they can't trigger rules. API tokens also get their project's own types (`ext.*`).",
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "eventTypes"
                  ],
                  "properties": {
                    "eventTypes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventType"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "token": []
          },
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "registerEventType",
        "summary": "Register an event type",
        "description": "Registers one of the project's own event types, `ext.<thing>.<past-tense verb>`, or updates it compatibly: within a version, data fields may only be added, and only as optional. Anything else needs a new `version`.",
        "security": [
          {
            "token": [
              "events:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewEventType"
              },
              "example": {
                "name": "ext.order.shipped",
                "description": "An order left the warehouse.",
                "data": [
                  {
                    "name": "orderId",
                    "type": "string",
                    "description": "The shop's order ID.",
                    "required": true
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The registered type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventType"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks events:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid, or an incompatible change within a version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/inboxes": {
      "get": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "listInboxes",
        "summary": "List your inboxes",
        "description": "The inboxes you've been granted access to, with your role in each.",
        "responses": {
          "200": {
            "description": "Your inboxes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "inboxes"
                  ],
                  "properties": {
                    "inboxes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboxGrant"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/labels": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "get": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "listLabels",
        "summary": "List labels",
        "description": "The inbox's labels (folders in a mail app): INBOX first, then special-use labels (Sent, Drafts, Trash, Spam, Archive), then the rest by name. A message can carry several labels.",
        "responses": {
          "200": {
            "description": "The labels.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "labels"
                  ],
                  "properties": {
                    "labels": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Label"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/labels/{label}/messages": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/label"
        }
      ],
      "get": {
        "tags": [
          "Messages"
        ],
        "operationId": "listMessages",
        "summary": "List a label's messages",
        "description": "Newest first. To get the next page, pass the response's `before` back as `before`; it's absent on the last page.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Only messages with a lower UID than this (a cursor from the previous page).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of messages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messages"
                  ],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessageSummary"
                      }
                    },
                    "before": {
                      "type": "integer",
                      "description": "Cursor for the next page."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/keys": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "get": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "getInboxKeys",
        "summary": "Get the inbox's keys",
        "description": "Every epoch key of the inbox, each sealed to your user key, which you set up in the web app. Messages name the epoch they were encrypted under (`keyEpoch`).",
        "responses": {
          "200": {
            "description": "The sealed epoch keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "epochs"
                  ],
                  "properties": {
                    "epochs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "epoch",
                          "wrapped"
                        ],
                        "properties": {
                          "epoch": {
                            "type": "integer"
                          },
                          "wrapped": {
                            "type": "string",
                            "contentEncoding": "base64",
                            "description": "The epoch's private key, sealed to your user key."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "You haven't set up your encryption key yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/messages/{msg}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        }
      ],
      "get": {
        "tags": [
          "Messages"
        ],
        "operationId": "getMessage",
        "summary": "Get a message",
        "description": "Metadata plus the message's data key, wrapped to the inbox epoch key. The body comes from `/raw`. The response carries an ETag (it changes with any change to the message, including data added later) and `Cache-Control: private, no-cache`: send `If-None-Match` to get 304 when nothing changed.",
        "responses": {
          "200": {
            "description": "The message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "keyEpoch",
                    "wrappedDek"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessageSummary"
                    },
                    "cc": {
                      "type": "string"
                    },
                    "replyTo": {
                      "type": "string"
                    },
                    "messageId": {
                      "type": "string",
                      "description": "The Message-ID header."
                    },
                    "inReplyTo": {
                      "type": "string"
                    },
                    "keyEpoch": {
                      "type": "integer",
                      "description": "The inbox epoch the message was encrypted under."
                    },
                    "wrappedDek": {
                      "type": "string",
                      "contentEncoding": "base64",
                      "description": "The message's data key, sealed to the epoch key."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "304": {
            "description": "Not modified: your copy is current."
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "The ETag of a copy you have.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/inboxes/{inbox}/messages/{msg}/raw": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        }
      ],
      "get": {
        "tags": [
          "Messages"
        ],
        "operationId": "getMessageRaw",
        "summary": "Get a message's encrypted content",
        "description": "The encrypted RFC 5322 message exactly as stored. Decrypt it with the data key from `GET /inboxes/{inbox}/messages/{msg}`. Messages never change, so the response may be kept by the browser indefinitely (`Cache-Control: private, max-age=31536000, immutable`).",
        "responses": {
          "200": {
            "description": "Ciphertext.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/octet-stream"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/messages/{msg}/flags": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        }
      ],
      "post": {
        "tags": [
          "Messages"
        ],
        "operationId": "updateFlags",
        "summary": "Change a message's flags",
        "description": "Adds and removes flags by their neutral names. Mail apps connected over IMAP, and other open web app tabs, see the change right away. Other flags (draft, junk, ...) are set by mail apps.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "add": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/WebFlag"
                    }
                  },
                  "remove": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/WebFlag"
                    }
                  }
                }
              },
              "example": {
                "add": [
                  "seen"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message's flags after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "flags"
                  ],
                  "properties": {
                    "flags": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Flag"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "flags:write"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/compose": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "get": {
        "tags": [
          "Sending"
        ],
        "operationId": "getCompose",
        "summary": "Get what's needed to send",
        "description": "The addresses the inbox receives on and the current epoch public key to seal outgoing messages to.",
        "responses": {
          "200": {
            "description": "Addresses and the sealing key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "addresses",
                    "epoch",
                    "epochPublic"
                  ],
                  "properties": {
                    "addresses": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "email"
                      }
                    },
                    "epoch": {
                      "type": "integer"
                    },
                    "epochPublic": {
                      "type": "string",
                      "contentEncoding": "base64",
                      "description": "The epoch's X25519 public key."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "send"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/send": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "post": {
        "tags": [
          "Sending"
        ],
        "operationId": "sendMessage",
        "summary": "Send a message",
        "description": "Sends a message the client sealed to the inbox's epoch key, bound to a draft ID. It goes through the same checks and limits as sending from a mail app, and a copy is filed in Sent. Up to 100 recipients and 25 MB.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "draftId",
                  "epoch",
                  "wrappedDek",
                  "ciphertext",
                  "from",
                  "to"
                ],
                "properties": {
                  "draftId": {
                    "type": "string",
                    "pattern": "^draft_[A-Za-z0-9_-]{16,64}$",
                    "description": "A new random ID per message; the ciphertext is bound to it."
                  },
                  "epoch": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "The epoch from `/compose` the message was sealed to."
                  },
                  "wrappedDek": {
                    "type": "string",
                    "contentEncoding": "base64"
                  },
                  "ciphertext": {
                    "type": "string",
                    "contentEncoding": "base64",
                    "description": "The RFC 5322 message, encrypted with the data key."
                  },
                  "from": {
                    "type": "string",
                    "format": "email",
                    "description": "A bare address the inbox may send as; it must match the message's From header."
                  },
                  "to": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "string",
                      "format": "email"
                    },
                    "description": "Every recipient (To, Cc and Bcc), as bare addresses."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sent"
                  ],
                  "properties": {
                    "sent": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The inbox can't send as `from`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Refused: a sending limit or policy, with the reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporary failure; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "send"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/events": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "streamInboxEvents",
        "summary": "Stream inbox changes",
        "description": "Server-Sent Events announcing changes to the inbox, so a client can update its lists without polling. Each event carries IDs only; fetch details through the API. Event names are `message.added`, `message.removed` and `message.flags`, and each event's data is a `LiveChange` JSON object. The server sends a comment every 15 seconds to keep the connection open and ends the stream after 10 minutes, so reconnect with a fresh token.",
        "responses": {
          "200": {
            "description": "An event stream.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "event: message.added\ndata: {\"type\":\"message.added\",\"labelId\":\"lbl_…\",\"msgId\":\"msg_…\",\"uid\":42}\n\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [
              "messages:read"
            ]
          },
          {
            "session": []
          }
        ]
      }
    },
    "/inboxes/{inbox}/messages/{msg}/content": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        }
      ],
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getMessageContent",
        "summary": "Read a message (decrypted)",
        "description": "The message decrypted and parsed: headers, the text and HTML bodies, and its attachments and inline parts (fetch each by `index`). Compatible-mode inboxes only; the read is recorded in the inbox's audit log (`access.content.read`). Never cached.",
        "security": [
          {
            "token": [
              "content:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "receivedAt",
                    "flags",
                    "headers",
                    "text",
                    "html",
                    "attachments"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "receivedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "flags": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Flag"
                      }
                    },
                    "keywords": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "headers": {
                      "type": "object",
                      "description": "Decoded headers: from, to, cc, replyTo, subject, date, messageId, inReplyTo, references (those present).",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "text": {
                      "type": "string",
                      "description": "The plain-text body (empty if none)."
                    },
                    "html": {
                      "type": "string",
                      "description": "The HTML body (empty if none). Untrusted: sanitize before display."
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The message couldn't be parsed; read it whole from /source.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/inboxes/{inbox}/messages/{msg}/source": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        }
      ],
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getMessageSource",
        "summary": "Get a message's source (decrypted)",
        "description": "The whole RFC 5322 message, decrypted. Compatible-mode inboxes only; audited like `/content`. Never cached.",
        "security": [
          {
            "token": [
              "content:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The message.",
            "content": {
              "message/rfc822": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/inboxes/{inbox}/messages/{msg}/attachments/{n}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        },
        {
          "$ref": "#/components/parameters/msg"
        },
        {
          "name": "n",
          "in": "path",
          "required": true,
          "description": "The attachment's `index` from `/content`.",
          "schema": {
            "type": "integer",
            "minimum": 0
          }
        }
      ],
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getAttachment",
        "summary": "Get an attachment (decrypted)",
        "description": "One attachment or inline part, with its content type and filename. Compatible-mode inboxes only; audited. Never cached.",
        "security": [
          {
            "token": [
              "content:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The attachment.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/octet-stream"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks the scope, or (decrypted content) the inbox is zero-access, or the caller is a web session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/inboxes/{inbox}/messages": {
      "parameters": [
        {
          "$ref": "#/components/parameters/inbox"
        }
      ],
      "post": {
        "tags": [
          "Sending"
        ],
        "operationId": "sendPlainMessage",
        "summary": "Send a message (plaintext)",
        "description": "Sends an RFC 5322 message as given: the same checks and limits as sending from a mail app, and a copy is filed in Sent. For API tokens; the web app seals its messages (`/send`). Up to 100 recipients and 25 MB.",
        "security": [
          {
            "token": [
              "send"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from",
                  "to",
                  "raw"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "email",
                    "description": "A bare address the inbox may send as; it must match the message's From header."
                  },
                  "to": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "string",
                      "format": "email"
                    },
                    "description": "Every recipient (To, Cc and Bcc), as bare addresses."
                  },
                  "raw": {
                    "type": "string",
                    "contentEncoding": "base64",
                    "description": "The RFC 5322 message."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sent"
                  ],
                  "properties": {
                    "sent": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The inbox can't send as `from`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Refused: a sending limit or policy, with the reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporary failure; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/events": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "publishEvent",
        "summary": "Publish an event",
        "description": "Publishes one event of a type the project registered. Events are plaintext and kept until the project is deleted, so keep them thin: IDs and small facts, never message content or secrets (data up to 4 KB). Send `Idempotency-Key` to make retries safe: the same key within 24 hours returns the first event.",
        "security": [
          {
            "token": [
              "events:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Up to 255 characters; the same key returns the first event.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "A registered ext.* type."
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "What the event is about, in your terms (e.g. an order number)."
                  },
                  "inbox": {
                    "type": "string",
                    "description": "An inbox it concerns (ibx_…), if any."
                  },
                  "data": {
                    "type": "object",
                    "description": "Fields as the type defines them; up to 4 KB."
                  }
                }
              },
              "example": {
                "type": "ext.order.shipped",
                "subject": "order 1001",
                "data": {
                  "orderId": "1001"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cursor": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "A retry with the same Idempotency-Key: the first event.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "duplicate": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks events:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "data is over 4 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Unregistered type, or data that doesn't match it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEvents",
        "summary": "Read the project's events",
        "description": "Every event of the project, oldest first, across its inboxes and the project itself: a page after a cursor, or with `Accept: text/event-stream` a live stream that resumes from `Last-Event-ID`. Each event's `id:` line in the stream is its cursor. You see inbox events for inboxes you may use; project-level events (access, domains, sending) need `project:read`; audit-only events (who read what) need `audit:read` and `audit=true`. Paged reads return events a few seconds old, so none arrive later behind the cursor; live streams send at once and may repeat an event after reconnecting, so dedupe by event `id`. The web app must name its `inboxes` and sees read state without who read it.",
        "security": [
          {
            "token": [
              "events:read"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "description": "Cursor: return events after it (the `cursor` of the previous page, or an event's stream `id`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "types",
            "in": "query",
            "description": "Comma-separated types or prefixes ending in `.*`, e.g. `message.*,ext.order.shipped`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inboxes",
            "in": "query",
            "description": "Comma-separated inbox IDs: only their events (and no project-level ones).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "audit",
            "in": "query",
            "description": "`true` includes audit-only events (needs audit:read).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, up to 500.",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "description": "Live streams: resume after this cursor.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page, or the live stream.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events",
                    "cursor"
                  ],
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "cursor": {
                      "type": "string",
                      "description": "Pass as after= for the next page."
                    }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "id: 2026-10-11T06:05:40.037000000Z~evt_…\nevent: mail.delivery.accepted\ndata: {\"id\":\"evt_…\",\"type\":\"mail.delivery.accepted\",…}\n\n"
              }
            }
          },
          "400": {
            "description": "The web app must name inboxes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing scope (events:read, or audit:read for audit=true).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "token": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API token, `k8hq_<token ID>_<secret>`. The security requirement of each operation names the scope it needs."
      },
      "session": {
        "type": "apiKey",
        "in": "header",
        "name": "x-k8hq-token",
        "description": "A session access token from signing in to the K8 HQ web app (app.k8hq.com only)."
      }
    },
    "parameters": {
      "inbox": {
        "name": "inbox",
        "in": "path",
        "required": true,
        "description": "Inbox ID (`ibx_…`).",
        "schema": {
          "type": "string",
          "pattern": "^ibx_[0-9A-Z]{26}$"
        }
      },
      "label": {
        "name": "label",
        "in": "path",
        "required": true,
        "description": "Label ID (`lbl_…`).",
        "schema": {
          "type": "string",
          "pattern": "^lbl_[0-9A-Z]{26}$"
        }
      },
      "msg": {
        "name": "msg",
        "in": "path",
        "required": true,
        "description": "Message ID (`msg_…`).",
        "schema": {
          "type": "string",
          "pattern": "^msg_[0-9A-Z]{26}$"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is malformed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No token, or it expired: sign in again.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found, or not yours.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "InboxGrant": {
        "type": "object",
        "required": [
          "inboxId",
          "role"
        ],
        "properties": {
          "inboxId": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "description": "Your role in the inbox, e.g. owner."
          },
          "label": {
            "type": "string",
            "description": "Display name, usually the inbox's main address."
          }
        }
      },
      "Label": {
        "type": "object",
        "required": [
          "id",
          "name",
          "total",
          "unread"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "specialUse": {
            "type": "string",
            "enum": [
              "\\Sent",
              "\\Drafts",
              "\\Trash",
              "\\Junk",
              "\\Archive"
            ],
            "description": "The label's role, as IMAP names it."
          },
          "total": {
            "type": "integer"
          },
          "unread": {
            "type": "integer"
          }
        }
      },
      "MessageSummary": {
        "type": "object",
        "required": [
          "id",
          "uid",
          "receivedAt",
          "flags",
          "size"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "uid": {
            "type": "integer",
            "description": "The message's number in this label (0 when fetched outside a label)."
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time"
          },
          "date": {
            "type": "string",
            "description": "The Date header."
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "flags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Flag"
            },
            "description": "The message's flags, by neutral name (the same names events use)."
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Custom keywords mail apps have set, e.g. `$label1`."
          },
          "size": {
            "type": "integer",
            "description": "Bytes."
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ]
          }
        }
      },
      "Flag": {
        "type": "string",
        "enum": [
          "seen",
          "answered",
          "flagged",
          "draft",
          "forwarded",
          "junk",
          "not_junk"
        ],
        "description": "A message flag, by its neutral name: the same vocabulary as events, whatever the channel. Mail apps' IMAP flags map onto it (`\\Seen` is seen, `$Junk` is junk)."
      },
      "WebFlag": {
        "type": "string",
        "enum": [
          "seen",
          "flagged",
          "answered"
        ],
        "description": "The flags the API can change."
      },
      "LiveChange": {
        "type": "object",
        "required": [
          "type",
          "msgId"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "message.added",
              "message.removed",
              "message.flags"
            ]
          },
          "labelId": {
            "type": "string",
            "description": "The label a message was added to or removed from."
          },
          "msgId": {
            "type": "string"
          },
          "uid": {
            "type": "integer",
            "description": "The message's number in the label (message.added)."
          },
          "flags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Flag"
            },
            "description": "The message's flags now (message.flags)."
          }
        }
      },
      "EventType": {
        "type": "object",
        "required": [
          "name",
          "version",
          "description",
          "source",
          "class",
          "visibility",
          "status",
          "data",
          "dataSchema"
        ],
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "message.received"
            ]
          },
          "version": {
            "type": "integer"
          },
          "description": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "subject": {
            "type": "string",
            "description": "What the event's subject holds."
          },
          "class": {
            "type": "string"
          },
          "visibility": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "experimental",
              "stable",
              "deprecated"
            ]
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "The data fields."
          },
          "dataSchema": {
            "type": "object",
            "description": "JSON Schema for the event's data."
          }
        }
      },
      "Attachment": {
        "type": "object",
        "required": [
          "index",
          "contentType",
          "size",
          "inline"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "size": {
            "type": "integer",
            "description": "Bytes, decoded."
          },
          "contentId": {
            "type": "string",
            "description": "For inline parts referenced from the HTML (`cid:`)."
          },
          "inline": {
            "type": "boolean"
          }
        }
      },
      "NewEventType": {
        "type": "object",
        "required": [
          "name",
          "description"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^ext\\.[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$"
          },
          "description": {
            "type": "string",
            "maxLength": 1000
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "default": 1
          },
          "data": {
            "type": "array",
            "maxItems": 32,
            "items": {
              "type": "object",
              "required": [
                "name",
                "type"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "string",
                    "integer",
                    "boolean",
                    "string[]"
                  ]
                },
                "description": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "A CloudEvents 1.0 event with K8 HQ's attributes.",
        "required": [
          "id",
          "type",
          "source",
          "time",
          "scope",
          "sequence",
          "actor",
          "class",
          "visibility"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          },
          "dataschema": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "description": "The project (prj_)."
          },
          "containerid": {
            "type": "string",
            "description": "The inbox it concerns, if any."
          },
          "sequence": {
            "type": "string",
            "description": "Its position in its own stream (the inbox, or the project)."
          },
          "actor": {
            "type": "string",
            "description": "Who acted: a member (mem_), a token (tok_) or K8 HQ (sys_…). Empty when removed for privacy."
          },
          "causationid": {
            "type": "string"
          },
          "correlationid": {
            "type": "string"
          },
          "class": {
            "type": "string"
          },
          "visibility": {
            "type": "string"
          },
          "data": {
            "type": "object"
          }
        }
      }
    }
  }
}
