openapi: 3.1.0
info:
  title: Convot Public API
  version: "1.0.0"
  description: |
    The Convot REST API gives programmatic access to a single Convot **app**:
    conversations, messages, contacts, help-center content, changelog, and roadmap.

    ## Authentication
    Every request needs an API key, created in the dashboard under
    **App Settings → API Keys**. The key has the form `prefix:secret` and is sent
    as a Bearer token:

    ```
    Authorization: Bearer ck_xxxxxxxxxxxxxxxx:secret
    ```

    The key is scoped to one app, so requests never include an app id — the key
    *is* the app. Keys are shown once at creation and are independently revocable.

    ## Scopes
    Each key is granted a set of `resource:action` scopes (e.g. `conversations:read`,
    `messages:write`). A request missing the required scope returns `403`.

    ## Errors
    Errors use a consistent envelope:
    ```json
    { "error": { "code": "not_found", "message": "Resource not found" } }
    ```

    ## Rate limits
    Limits are enforced per key, aggregated per app, and as a per-app daily quota.
    Exceeding a limit returns `429` with `Retry-After` and `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers. Defaults: 120 req/min per
    key (40/min for writes), 600/min aggregated across an app's keys, 10,000/day per
    app. Contact support to raise them.

servers:
  - url: https://app.convot.io/v1
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Conversations
  - name: Messages
  - name: Contacts
  - name: Changelog
  - name: Roadmap
  - name: Help Center
  - name: Webhooks
  - name: Uploads

paths:
  /conversations:
    get:
      tags: [Conversations]
      summary: List conversations
      x-mcp-tool: list_conversations
      description: "Requires scope `conversations:read`."
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, pending, resolved] } }
        - { name: contact_id, in: query, schema: { type: integer } }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of conversations
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversations: { type: array, items: { $ref: "#/components/schemas/Conversation" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Conversations]
      summary: Create a conversation
      x-mcp-tool: create_conversation
      description: "Requires scope `conversations:write`. Provide `contact_id` or `email`; optionally seed the first message with `content`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                contact_id: { type: integer, description: "Existing contact in this app." }
                email: { type: string, description: "Find-or-create a contact by email (used if contact_id omitted)." }
                name: { type: string }
                content: { type: string, description: "Optional first message body." }
                operator_id: { type: integer, description: "Send the seeded message as a real app user." }
      responses:
        "201":
          description: Created
          content: { application/json: { schema: { type: object, properties: { conversation: { $ref: "#/components/schemas/Conversation" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /conversations/{id}:
    get:
      tags: [Conversations]
      summary: Get a conversation
      x-mcp-tool: get_conversation
      description: "Requires scope `conversations:read`. `id` is the conversation UUID."
      parameters: [ { $ref: "#/components/parameters/Uuid" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { conversation: { $ref: "#/components/schemas/Conversation" } } } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /conversations/{id}/resolve:
    post:
      tags: [Conversations]
      summary: Resolve a conversation
      x-mcp-tool: resolve_conversation
      description: "Requires scope `conversations:write`."
      parameters: [ { $ref: "#/components/parameters/Uuid" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { conversation: { $ref: "#/components/schemas/Conversation" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /conversations/{id}/reopen:
    post:
      tags: [Conversations]
      summary: Reopen a conversation
      x-mcp-tool: reopen_conversation
      description: "Requires scope `conversations:write`."
      parameters: [ { $ref: "#/components/parameters/Uuid" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { conversation: { $ref: "#/components/schemas/Conversation" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /conversations/{id}/messages:
    get:
      tags: [Messages]
      summary: List messages (cursor-paginated)
      x-mcp-tool: list_messages
      description: "Requires scope `messages:read`. Newest first. Excludes private notes and activity events."
      parameters:
        - { $ref: "#/components/parameters/Uuid" }
        - { name: limit, in: query, schema: { type: integer, default: 30, minimum: 1, maximum: 100 } }
        - { name: before_id, in: query, schema: { type: integer }, description: "Return messages with id below this cursor." }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages: { type: array, items: { $ref: "#/components/schemas/Message" } }
                  meta: { $ref: "#/components/schemas/CursorMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Messages]
      summary: Send a message
      x-mcp-tool: send_message
      description: "Requires scope `messages:write`. Without `operator_id` the message is attributed to the app's API identity."
      parameters: [ { $ref: "#/components/parameters/Uuid" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content: { type: string, description: "Required unless attachment_ids is given." }
                operator_id: { type: integer, description: "Send as a real app user." }
                attachment_ids:
                  type: array
                  items: { type: integer }
                  description: "Ids from POST /uploads to attach to this message."
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { message: { $ref: "#/components/schemas/Message" } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      x-mcp-tool: search_contacts
      description: "Requires scope `contacts:read`."
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "Search name and email." }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts: { type: array, items: { $ref: "#/components/schemas/Contact" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Contacts]
      summary: Create or update a contact
      x-mcp-tool: upsert_contact
      description: "Requires scope `contacts:write`. Find-or-create by `email` or `external_id`."
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ContactInput" } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { contact: { $ref: "#/components/schemas/Contact" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /contacts/{id}:
    get:
      tags: [Contacts]
      summary: Get a contact
      x-mcp-tool: get_contact
      description: "Requires scope `contacts:read`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { contact: { $ref: "#/components/schemas/Contact" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Contacts]
      summary: Update a contact
      description: "Requires scope `contacts:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ContactInput" } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { contact: { $ref: "#/components/schemas/Contact" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /changelog:
    get:
      tags: [Changelog]
      summary: List changelog entries
      description: "Requires scope `changelog:read`."
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [draft, visible, hidden] } }
        - $ref: "#/components/parameters/Page"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  changelog: { type: array, items: { $ref: "#/components/schemas/ChangelogEntry" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Changelog]
      summary: Create a changelog entry
      description: "Requires scope `changelog:write`."
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ChangelogInput" } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { changelog_entry: { $ref: "#/components/schemas/ChangelogEntry" } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /changelog/{id}:
    get:
      tags: [Changelog]
      summary: Get a changelog entry
      description: "Requires scope `changelog:read`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { changelog_entry: { $ref: "#/components/schemas/ChangelogEntry" } } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Changelog]
      summary: Update a changelog entry
      description: "Requires scope `changelog:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ChangelogInput" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { changelog_entry: { $ref: "#/components/schemas/ChangelogEntry" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Changelog]
      summary: Delete a changelog entry
      description: "Requires scope `changelog:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "204": { description: Deleted }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /roadmap_items:
    get:
      tags: [Roadmap]
      summary: List roadmap items
      description: "Requires scope `roadmap:read`. Sorted by votes."
      parameters:
        - { name: visibility, in: query, schema: { type: string, enum: [draft, visible, hidden] } }
        - $ref: "#/components/parameters/Page"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  roadmap_items: { type: array, items: { $ref: "#/components/schemas/RoadmapItem" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Roadmap]
      summary: Create a roadmap item (feature request)
      description: "Requires scope `roadmap:write`."
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/RoadmapInput" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { roadmap_item: { $ref: "#/components/schemas/RoadmapItem" } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /roadmap_items/{id}:
    get:
      tags: [Roadmap]
      summary: Get a roadmap item
      description: "Requires scope `roadmap:read`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { roadmap_item: { $ref: "#/components/schemas/RoadmapItem" } } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Roadmap]
      summary: Update a roadmap item
      description: "Requires scope `roadmap:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/RoadmapInput" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { roadmap_item: { $ref: "#/components/schemas/RoadmapItem" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /articles:
    get:
      tags: [Help Center]
      summary: List help-center articles
      x-mcp-tool: search_articles
      description: "Requires scope `helpdesk:read`. `q` runs a fuzzy, typo-tolerant search."
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: category_id, in: query, schema: { type: integer } }
        - $ref: "#/components/parameters/Page"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  articles: { type: array, items: { $ref: "#/components/schemas/Article" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Help Center]
      summary: Create an article
      description: "Requires scope `helpdesk:write`."
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ArticleInput" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { article: { $ref: "#/components/schemas/Article" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /articles/{id}:
    get:
      tags: [Help Center]
      summary: Get an article
      x-mcp-tool: get_article
      description: "Requires scope `helpdesk:read`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { article: { $ref: "#/components/schemas/Article" } } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Help Center]
      summary: Update an article
      description: "Requires scope `helpdesk:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ArticleInput" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { article: { $ref: "#/components/schemas/Article" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Help Center]
      summary: Delete an article
      description: "Requires scope `helpdesk:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "204": { description: Deleted }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /categories:
    get:
      tags: [Help Center]
      summary: List categories
      description: "Requires scope `helpdesk:read`."
      parameters: [ { $ref: "#/components/parameters/Page" } ]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories: { type: array, items: { $ref: "#/components/schemas/Category" } }
                  meta: { $ref: "#/components/schemas/OffsetMeta" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Help Center]
      summary: Create a category
      description: "Requires scope `helpdesk:write`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                color: { type: string, description: "Hex color; defaults to #6366F1." }
                language: { type: string, default: en }
                position: { type: integer }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { category: { $ref: "#/components/schemas/Category" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /categories/{id}:
    get:
      tags: [Help Center]
      summary: Get a category
      description: "Requires scope `helpdesk:read`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { category: { $ref: "#/components/schemas/Category" } } } } } }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook subscriptions
      description: "Requires scope `webhooks:read`."
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { webhooks: { type: array, items: { $ref: "#/components/schemas/Webhook" } } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Webhooks]
      summary: Create a webhook subscription
      description: "Requires scope `webhooks:write`. A signing secret is generated and returned. Deliveries are signed with HMAC-SHA256."
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookInput" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { webhook: { $ref: "#/components/schemas/Webhook" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /webhooks/{id}:
    patch:
      tags: [Webhooks]
      summary: Update a webhook
      description: "Requires scope `webhooks:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookInput" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { webhook: { $ref: "#/components/schemas/Webhook" } } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      description: "Requires scope `webhooks:write`."
      parameters: [ { $ref: "#/components/parameters/IntId" } ]
      responses:
        "204": { description: Deleted }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /uploads:
    post:
      tags: [Uploads]
      summary: Upload an image
      description: |
        Requires scope `uploads:write`. Multipart upload of an image
        (jpeg, png, webp, or gif). Returns a hosted URL and an id you can pass as
        `attachment_ids` when sending a message, or embed in article/changelog content.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { upload: { $ref: "#/components/schemas/Upload" } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: "prefix:secret"

  parameters:
    Uuid: { name: id, in: path, required: true, schema: { type: string }, description: "Conversation UUID." }
    IntId: { name: id, in: path, required: true, schema: { type: integer } }
    Page: { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
    PerPage: { name: per_page, in: query, schema: { type: integer, default: 30, minimum: 1, maximum: 100 } }

  responses:
    BadRequest: { description: Invalid request, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Unauthorized: { description: Missing, invalid, or revoked API key, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Forbidden: { description: API key lacks the required scope, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    NotFound: { description: Resource not found (or not in this app), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Unprocessable: { description: Validation failed, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds until the limit resets. }
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        X-RateLimit-Reset: { schema: { type: integer }, description: Unix epoch seconds. }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, examples: [not_found, forbidden, rate_limited, bad_request, unprocessable_entity, unauthorized] }
            message: { type: string }
            details: { type: array, items: { type: string }, description: "Present on validation errors." }

    OffsetMeta:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        has_more: { type: boolean }

    CursorMeta:
      type: object
      properties:
        limit: { type: integer }
        has_more: { type: boolean }
        next_before_id: { type: [integer, "null"], description: "Pass as before_id to fetch the next page." }

    Conversation:
      type: object
      properties:
        id: { type: string, description: "Conversation UUID (the public id)." }
        status: { type: string, enum: [open, pending, resolved] }
        channel: { type: [string, "null"] }
        subject: { type: [string, "null"] }
        contact_id: { type: integer }
        assignee_name: { type: [string, "null"] }
        messages_count: { type: integer }
        unread_count: { type: integer }
        last_activity_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        tags: { type: array, items: { type: string } }

    Message:
      type: object
      properties:
        id: { type: integer }
        conversation_id: { type: string }
        type: { type: string, enum: [incoming, outgoing] }
        content: { type: [string, "null"] }
        content_type: { type: [string, "null"] }
        sender:
          type: object
          properties:
            type: { type: string, enum: [operator, contact, api] }
            name: { type: [string, "null"] }
            avatar_url: { type: [string, "null"] }
        attachments:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              file_url: { type: [string, "null"] }
              file_name: { type: [string, "null"] }
              file_type: { type: [string, "null"] }
        meta: { type: object }
        created_at: { type: string, format: date-time }

    Contact:
      type: object
      properties:
        id: { type: integer }
        name: { type: [string, "null"] }
        email: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        company: { type: [string, "null"] }
        external_id: { type: [string, "null"] }
        avatar_url: { type: [string, "null"] }
        country: { type: [string, "null"] }
        country_code: { type: [string, "null"] }
        city: { type: [string, "null"] }
        custom_data: { type: [object, "null"] }
        last_seen_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }

    ContactInput:
      type: object
      properties:
        email: { type: string }
        name: { type: string }
        phone: { type: string }
        company: { type: string }
        external_id: { type: string }
        country: { type: string }
        country_code: { type: string }
        city: { type: string }
        custom_data: { type: object }

    ChangelogEntry:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        slug: { type: string }
        content: { type: [string, "null"] }
        type: { type: [string, "null"], enum: [feature, improvement, fix, null] }
        type_label: { type: [string, "null"] }
        type_color: { type: [string, "null"] }
        status: { type: string, enum: [draft, visible, hidden] }
        published: { type: boolean }
        published_at: { type: [string, "null"], format: date-time }
        language: { type: string }
        views_count: { type: integer }
        public_url: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ChangelogInput:
      type: object
      properties:
        title: { type: string }
        content: { type: string }
        entry_type: { type: string, enum: [feature, improvement, fix] }
        changelog_type_id: { type: integer, description: "Must belong to this app." }
        status: { type: string, enum: [draft, visible, hidden] }
        language: { type: string, default: en }
        published_at: { type: string, format: date-time }

    RoadmapItem:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        slug: { type: string }
        body: { type: [string, "null"] }
        status_label: { type: [string, "null"] }
        status_color: { type: [string, "null"] }
        status_public: { type: boolean }
        visibility: { type: string, enum: [draft, visible, hidden] }
        published: { type: boolean }
        language: { type: string }
        votes_count: { type: integer }
        comments_count: { type: integer }
        public_url: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    RoadmapInput:
      type: object
      properties:
        title: { type: string }
        body: { type: string }
        visibility: { type: string, enum: [draft, visible, hidden] }
        roadmap_status_id: { type: integer, description: "Must belong to this app." }
        language: { type: string, default: en }

    Article:
      type: object
      properties:
        id: { type: integer }
        category_id: { type: [integer, "null"] }
        title: { type: string }
        slug: { type: string }
        summary: { type: [string, "null"] }
        content: { type: [string, "null"] }
        status: { type: string }
        language: { type: string }
        position: { type: [integer, "null"] }
        meta_title: { type: [string, "null"] }
        meta_description: { type: [string, "null"] }
        views_count: { type: [integer, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ArticleInput:
      type: object
      properties:
        title: { type: string }
        content: { type: string }
        summary: { type: string }
        category_id: { type: integer, description: "Must belong to this app." }
        status: { type: string }
        language: { type: string, default: en }
        meta_title: { type: string }
        meta_description: { type: string }
        position: { type: integer }

    Category:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        slug: { type: string }
        color: { type: string }
        language: { type: string }
        position: { type: [integer, "null"] }
        articles_count: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string }
        events: { type: array, items: { type: string } }
        enabled: { type: boolean }
        secret: { type: string, description: "Sign deliveries are verified against this (HMAC-SHA256)." }
        last_status: { type: [string, "null"] }
        last_delivered_at: { type: [string, "null"], format: date-time }
        last_error: { type: [string, "null"] }
        created_at: { type: string, format: date-time }

    WebhookInput:
      type: object
      properties:
        url: { type: string, description: "Must start with http:// or https://" }
        enabled: { type: boolean }
        events:
          type: array
          description: "Subset of the supported events."
          items:
            type: string
            enum:
              - message.sent
              - message.received
              - message.deleted
              - conversation.created
              - conversation.assigned
              - conversation.resolved
              - conversation.reopened
              - contact.created
              - contact.identified

    Upload:
      type: object
      properties:
        id: { type: integer }
        url: { type: [string, "null"] }
        file_name: { type: [string, "null"] }
        content_type: { type: [string, "null"] }
        file_size: { type: [integer, "null"] }
        created_at: { type: string, format: date-time }
