openapi: 3.1.0
info:
  title: VERA Partner API
  version: "2026-10-09"
  summary: Governed access to VERA, nervous system intelligence, for partner platforms.
  description: |
    The VERA Partner API lets approved platforms bring VERA into their products.
    VERA listens first, helps people settle, then helps with the next useful thing.

    **Safety is built into the API, not left to the integration.**
    - VERA's identity and safety rules are applied on every request and cannot be changed by partners.
    - Crisis signals trigger a fixed, psychologist-reviewed protocol and crisis resources that the partner must display.
    - VERA can only *propose* actions. Each one needs the person's explicit approval before anything happens.
    - Every partner's data is isolated. Conversations are never used to train models.

    Authentication: `Authorization: Bearer vera_sk_...`. All requests and responses are JSON.
  contact:
    name: VERANeural Partner Support
    url: https://veraneural.com/contact
  license:
    name: Proprietary. Access under the VERANeural Partner Agreement.
    url: https://veraneural.com/terms
servers:
  - url: https://api.veraneural.com
    description: Production
security:
  - apiKey: []
tags:
  - name: System
    description: Service status and the API description.
  - name: Conversations
    description: A conversation between VERA and one person on your platform.
  - name: Messages
    description: Talk with VERA. Every reply carries a safety assessment.
  - name: Actions
    description: Proposals from VERA that only happen after the person approves.
  - name: Tools
    description: The actions VERA may propose on your platform.
  - name: Webhooks
    description: Signed notifications about actions and safety events.
  - name: Audit
    description: Your account’s audit trail.

paths:
  /v1/health:
    get:
      tags: [System]
      summary: Service health
      operationId: health
      security: []
      responses:
        "200":
          description: The API is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  version: { type: string, example: "2026-10-09" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/conversations:
    post:
      tags: [Conversations]
      summary: Start a conversation
      description: |
        Starts a conversation between VERA and one person on your platform.
        `end_user_id` must be an opaque ID from your system, never a name, email address or phone number.
        Set `country` so VERA gives the right crisis numbers.
      operationId: createConversation
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateConversation" }
      responses:
        "201":
          description: Created.
          content: { application/json: { schema: { $ref: "#/components/schemas/Conversation" } } }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/conversations/{conversation_id}:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    get:
      tags: [Conversations]
      summary: Get a conversation
      operationId: getConversation
      responses:
        "200":
          description: The conversation.
          content: { application/json: { schema: { $ref: "#/components/schemas/Conversation" } } }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Conversations]
      summary: Delete a conversation
      description: Permanently deletes the conversation, its messages and its actions. Use this when a person asks for their data to be deleted.
      operationId: deleteConversation
      responses:
        "204": { description: Deleted. }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/conversations/{conversation_id}/messages:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    post:
      tags: [Messages]
      summary: Send a message to VERA
      description: |
        Sends the person's message and returns VERA's reply.

        Always check `safety` in the response:
        - `crisis`: show `message.content` and **all** `safety.resources` prominently. `display_required` is true. Do not hide, shorten or delay them.
        - `elevated`: the conversation is in a safety hold. Actions are paused; show the resources where practical.
        - `none`: normal reply.

        `proposed_actions` lists actions VERA suggests. Nothing happens until the person approves one.
      operationId: sendMessage
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessage" }
      responses:
        "200":
          description: VERA's reply.
          content: { application/json: { schema: { $ref: "#/components/schemas/VeraReply" } } }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/EngineUnavailable" }
    get:
      tags: [Messages]
      summary: List messages
      operationId: listMessages
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        "200":
          description: Messages, oldest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Message" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/conversations/{conversation_id}/actions:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    get:
      tags: [Actions]
      summary: List a conversation's actions
      operationId: listActions
      responses:
        "200":
          description: Actions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Action" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/actions/{action_id}:
    parameters: [{ $ref: "#/components/parameters/ActionId" }]
    get:
      tags: [Actions]
      summary: Get an action
      operationId: getAction
      responses:
        "200":
          description: The action.
          content: { application/json: { schema: { $ref: "#/components/schemas/Action" } } }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/actions/{action_id}/approve:
    parameters: [{ $ref: "#/components/parameters/ActionId" }]
    post:
      tags: [Actions]
      summary: Record the person's approval
      description: |
        Call this only after the person has seen the action's summary in your interface and explicitly approved it.
        `end_user_id` must match the conversation's person. Actions expire after 24 hours.
        After approval, your platform carries out the action, then reports the outcome with `/result`.
      operationId: approveAction
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/Decision" } } }
      responses:
        "200":
          description: Approved.
          content: { application/json: { schema: { $ref: "#/components/schemas/Action" } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/actions/{action_id}/reject:
    parameters: [{ $ref: "#/components/parameters/ActionId" }]
    post:
      tags: [Actions]
      summary: Record the person's rejection
      operationId: rejectAction
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/Decision" } } }
      responses:
        "200":
          description: Rejected.
          content: { application/json: { schema: { $ref: "#/components/schemas/Action" } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/actions/{action_id}/result:
    parameters: [{ $ref: "#/components/parameters/ActionId" }]
    post:
      tags: [Actions]
      summary: Report what happened
      description: Tell VERA whether an approved action succeeded. VERA sees this on the next turn, so it never claims something happened that didn't.
      operationId: reportActionResult
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ActionResult" } } }
      responses:
        "200":
          description: Recorded.
          content: { application/json: { schema: { $ref: "#/components/schemas/Action" } } }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/tools:
    get:
      tags: [Tools]
      summary: List your tools
      operationId: listTools
      responses:
        "200":
          description: Tools.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Tool" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/tools/{name}:
    parameters:
      - name: name
        in: path
        required: true
        schema: { type: string, pattern: "^[a-z][a-z0-9_]{1,47}$" }
    put:
      tags: [Tools]
      summary: Register or update a tool
      description: |
        Declares an action VERA may *propose* on your platform, such as `schedule_meeting` or `create_invoice_draft`.
        VERA never executes tools. Your platform does, after the person approves.
        Arguments VERA proposes are checked against `input_schema`; invalid proposals are discarded.
      operationId: putTool
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ToolInput" } } }
      responses:
        "200":
          description: Saved.
          content: { application/json: { schema: { $ref: "#/components/schemas/Tool" } } }
        "400": { $ref: "#/components/responses/InvalidRequest" }
    delete:
      tags: [Tools]
      summary: Remove a tool
      operationId: deleteTool
      responses:
        "204": { description: Removed. }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks:
    post:
      tags: [Webhooks]
      summary: Add a webhook endpoint
      description: |
        Receive signed events. The `secret` is returned only once.
        Each delivery has `VERA-Signature: t=<unix>,v1=<hex>`, where v1 = HMAC-SHA256(secret, "<t>.<raw body>").
        Verify it, and reject timestamps older than 5 minutes.
      operationId: createWebhook
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/CreateWebhook" } } }
      responses:
        "201":
          description: Created. Store the secret now.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookEndpoint"
                  - type: object
                    properties:
                      secret: { type: string, example: whsec_... }
        "400": { $ref: "#/components/responses/InvalidRequest" }
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      responses:
        "200":
          description: Endpoints (secrets are never shown again).
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/webhooks/{webhook_id}:
    parameters:
      - name: webhook_id
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Webhooks]
      summary: Remove a webhook endpoint
      operationId: deleteWebhook
      responses:
        "204": { description: Removed. }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/audit-events:
    get:
      tags: [Audit]
      summary: Your audit trail
      description: Requests, approvals, safety events and webhook failures for your account. Never contains message content.
      operationId: listAuditEvents
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: Newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/AuditEvent" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

webhooks:
  action.proposed:
    post:
      operationId: onActionProposed
      summary: VERA proposed an action
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEvent" } } }
      responses: { "200": { description: Acknowledge with any 2xx. } }
  action.approved:
    post:
      operationId: onActionApproved
      summary: The person approved an action. Carry it out now.
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEvent" } } }
      responses: { "200": { description: Acknowledge with any 2xx. } }
  action.rejected:
    post:
      operationId: onActionRejected
      summary: The person rejected an action
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEvent" } } }
      responses: { "200": { description: Acknowledge with any 2xx. } }
  action.completed:
    post:
      operationId: onActionCompleted
      summary: A result was recorded for an action
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEvent" } } }
      responses: { "200": { description: Acknowledge with any 2xx. } }
  safety.crisis_detected:
    post:
      operationId: onSafetyCrisisDetected
      summary: A person may be in crisis
      description: Contains the conversation and end-user IDs only, never message content. Route it to your human support or safeguarding process if you have one.
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEvent" } } }
      responses: { "200": { description: Acknowledge with any 2xx. } }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: vera_sk_...
      description: Partner API key. Keep it on your server; never ship it in a browser or mobile app.

  parameters:
    ConversationId:
      name: conversation_id
      in: path
      required: true
      schema: { type: string, example: conv_8xZr0aQ1 }
    ActionId:
      name: action_id
      in: path
      required: true
      schema: { type: string, example: act_3kP0b2Lm }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Retry safely. The same key within 24 hours returns the original response instead of processing twice.
      schema: { type: string, minLength: 8, maxLength: 128 }

  schemas:
    CreateConversation:
      type: object
      required: [end_user_id]
      properties:
        end_user_id: { type: string, maxLength: 128, pattern: "^[A-Za-z0-9._:@-]+$", example: user_8f3a21 }
        locale: { type: string, default: en, example: en-US }
        country: { type: [string, "null"], pattern: "^[A-Z]{2}$", example: US, description: Used to choose crisis numbers. }
        metadata: { type: object, additionalProperties: { type: string, maxLength: 500 }, maxProperties: 20 }
    Conversation:
      type: object
      properties:
        id: { type: string }
        object: { const: conversation }
        end_user_id: { type: string }
        locale: { type: string }
        country: { type: [string, "null"] }
        metadata: { type: object, additionalProperties: { type: string } }
        safety_hold: { type: boolean, description: True for a period after a crisis signal. Actions are paused. }
        created_at: { type: string, format: date-time }
    SendMessage:
      type: object
      required: [content]
      properties:
        content: { type: string, minLength: 1, maxLength: 8000, example: "Big week. I'm feeling stretched thin." }
        context:
          type: string
          maxLength: 4000
          description: Optional reference information from your platform (for example, today's schedule). Treated as data, never as instructions.
    VeraReply:
      type: object
      properties:
        object: { const: vera.reply }
        conversation_id: { type: string }
        message:
          type: object
          properties:
            id: { type: string }
            role: { const: assistant }
            content: { type: string }
            created_at: { type: string, format: date-time }
        safety: { $ref: "#/components/schemas/Safety" }
        proposed_actions: { type: array, items: { $ref: "#/components/schemas/Action" } }
    Safety:
      type: object
      properties:
        level: { type: string, enum: [none, elevated, crisis] }
        protocol: { type: string, example: crisis_v1 }
        display_required: { type: boolean, description: "When true, you must show the reply and every resource prominently." }
        resources:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: 988 Suicide & Crisis Lifeline }
              contact: { type: string, example: Call or text 988 }
              description: { type: string }
    Message:
      type: object
      properties:
        id: { type: string }
        object: { const: message }
        role: { type: string, enum: [user, assistant] }
        content: { type: string }
        safety_level: { type: string, enum: [none, elevated, crisis] }
        created_at: { type: string, format: date-time }
    Action:
      type: object
      properties:
        id: { type: string }
        object: { const: action }
        conversation_id: { type: string }
        tool: { type: string, example: schedule_meeting }
        arguments: { type: object }
        summary: { type: string, description: Plain-language summary to show the person before they decide. }
        status: { type: string, enum: [pending_approval, approved, rejected, expired, completed, failed] }
        proposed_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        decided_at: { type: [string, "null"], format: date-time }
        decided_by: { type: [string, "null"] }
        decision_note: { type: [string, "null"] }
        result: { type: [object, "null"] }
        completed_at: { type: [string, "null"], format: date-time }
    Decision:
      type: object
      required: [end_user_id]
      properties:
        end_user_id: { type: string, description: Must be the conversation's person. }
        note: { type: string, maxLength: 500 }
    ActionResult:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [completed, failed] }
        message: { type: string, maxLength: 1000, example: Booked for 3pm Thursday. }
        output: { type: object }
    ToolInput:
      type: object
      required: [description, input_schema]
      properties:
        description: { type: string, minLength: 10, maxLength: 1000, example: Schedule a meeting on the person's calendar. }
        input_schema:
          type: object
          required: [type, properties]
          properties:
            type: { const: object }
            properties:
              type: object
              additionalProperties:
                type: object
                required: [type]
                properties:
                  type: { type: string, enum: [string, number, integer, boolean] }
                  description: { type: string }
                  enum: { type: array }
            required: { type: array, items: { type: string } }
    Tool:
      type: object
      properties:
        object: { const: tool }
        name: { type: string }
        description: { type: string }
        input_schema: { type: object }
        created_at: { type: string, format: date-time }
    CreateWebhook:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri, example: https://hooks.partner.com/vera }
        events:
          type: array
          minItems: 1
          items: { type: string, enum: [action.proposed, action.approved, action.rejected, action.completed, safety.crisis_detected] }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string }
        object: { const: webhook_endpoint }
        url: { type: string }
        events: { type: array, items: { type: string } }
        created_at: { type: string, format: date-time }
    WebhookEvent:
      type: object
      properties:
        id: { type: string, example: evt_9Xf2 }
        type: { type: string }
        created_at: { type: string, format: date-time }
        data: { type: object }
    AuditEvent:
      type: object
      properties:
        id: { type: string }
        event: { type: string }
        method: { type: [string, "null"] }
        path: { type: [string, "null"] }
        status: { type: [integer, "null"] }
        request_id: { type: [string, "null"] }
        detail: { type: [object, "null"] }
        created_at: { type: string, format: date-time }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type: { type: string, enum: [invalid_request, authentication_error, permission_error, not_found, conflict, rate_limited, engine_unavailable, server_error] }
            message: { type: string }
            request_id: { type: string }

  responses:
    InvalidRequest:
      description: The request is not valid.
      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: The key lacks a scope, or the person does not match.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Not found (or belongs to another partner).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Conflict:
      description: The action is not in a state that allows this.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: Too many requests. See Retry-After.
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    EngineUnavailable:
      description: VERA is temporarily unavailable. Retry with the same Idempotency-Key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
