openapi: 3.1.0
info:
  title: retainr API
  version: "1"
  description: >
    Persistent AI agent memory for Make.com, n8n, and Zapier.
    Store, search, and recall context across automation workflow runs.
  contact:
    url: https://retainr.dev
  license:
    name: Proprietary

servers:
  - url: https://api.retainr.dev
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: rec_live_<base58>

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:   { type: string, example: validation_error }
        message: { type: string, example: "content is required" }

    Memory:
      type: object
      properties:
        id:         { type: string, example: mem_8xKp2mQ4nR }
        content:    { type: string }
        namespace:  { type: string, example: "customer:alice" }
        metadata:   { type: object }
        tags:       { type: array, items: { type: string } }
        ttl_at:     { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    MemoryResult:
      allOf:
        - $ref: "#/components/schemas/Memory"
        - type: object
          properties:
            score: { type: number, format: float, example: 0.94 }

    StoreRequest:
      type: object
      required: [content, namespace]
      properties:
        content:
          type: string
          maxLength: 32768
          description: Text to embed and store.
        namespace:
          type: string
          example: "customer:alice"
          description: >
            Grouping key. Any string works: "customer:alice", "project:q1",
            "session:xyz". The sole way to partition memories.
        tags:
          type: array
          items: { type: string, maxLength: 64 }
          maxItems: 20
        ttl_seconds:
          type: integer
          maximum: 31536000
          description: Seconds until auto-deletion. Omit for permanent storage.
        dedup_threshold:
          type: number
          minimum: 0
          maximum: 1
          description: >
            Cosine similarity threshold (0–1). If an existing memory in the same
            namespace exceeds this similarity, it is updated instead of
            creating a duplicate. 0 = always insert.
        metadata:
          type: object
          description: Arbitrary JSON metadata.

    StoreResponse:
      type: object
      properties:
        id:           { type: string }
        workspace_id: { type: string }
        namespace:    { type: string }
        created_at:   { type: string, format: date-time }
        ttl_at:       { type: string, format: date-time }
        deduplicated: { type: boolean }

    SearchRequest:
      type: object
      required: [query]
      properties:
        query:
          type: string
          description: Natural language query for semantic search.
        namespace:
          type: string
          description: Filter to a specific namespace.
        tags:
          type: array
          items: { type: string }
        limit:
          type: integer
          default: 10
          maximum: 100
          description: Max results. Default 10, max 100.
        threshold:
          type: number
          default: 0.5
          description: Minimum cosine similarity (0–1). Default 0.5.

    SearchResponse:
      type: object
      properties:
        results:
          type: array
          items: { $ref: "#/components/schemas/MemoryResult" }
        total: { type: integer }

    ContextRequest:
      type: object
      required: [query]
      properties:
        query:
          type: string
          description: Current user message or topic to retrieve context for.
        namespace:
          type: string
          description: Filter to a namespace.
        tags:       { type: array, items: { type: string } }
        limit:
          type: integer
          default: 5
          maximum: 20
        threshold:
          type: number
          default: 0.35
          description: Minimum similarity. Default 0.35 (broader than search).
        format:
          type: string
          enum: [system_prompt, bullet_list, numbered_list]
          default: system_prompt
          description: >
            Output format. "system_prompt" produces the most natural LLM injection.

    ContextResponse:
      type: object
      properties:
        context:
          type: string
          description: Formatted context string, ready to inject into an LLM system prompt.
        memories_used: { type: integer }
        token_estimate:
          type: integer
          description: Approximate token count for context window budgeting.

    ListResponse:
      type: object
      properties:
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
        total:  { type: integer }
        limit:  { type: integer }
        offset: { type: integer }

    DeleteRequest:
      type: object
      description: At least one filter is required.
      properties:
        namespace:  { type: string }
        tags:       { type: array, items: { type: string } }

    DeleteResponse:
      type: object
      properties:
        deleted: { type: integer }

    WorkspaceInfo:
      type: object
      properties:
        workspace_id:   { type: string }
        name:           { type: string }
        email:          { type: string }
        plan:           { type: string, example: free }
        ops_this_month: { type: integer }
        ops_limit:      { type: integer }
        memory_count:   { type: integer }
        api_keys:
          type: array
          items:
            type: object
            properties:
              id:           { type: string }
              name:         { type: string }
              prefix:       { type: string }
              last_used_at: { type: string, format: date-time }
              created_at:   { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    WebhookRegisterRequest:
      type: object
      required: [name, url, event_type]
      properties:
        name:             { type: string, description: Human label. }
        url:              { type: string, format: uri, description: "Must be https://." }
        event_type:       { type: string, enum: [memory.created, memory.watch] }
        namespace_filter: { type: string, description: Only fire for memories in this namespace. }
        watch_query:
          type: string
          description: Required for memory.watch. Phrase to match against incoming memories.
        watch_threshold:
          type: number
          default: 0.75
          minimum: 0.5
          maximum: 1.0
        secret:
          type: string
          description: HMAC secret for signature verification. Auto-generated if omitted.

    Webhook:
      type: object
      properties:
        id:                   { type: string }
        workspace_id:         { type: string }
        name:                 { type: string }
        url:                  { type: string }
        event_type:           { type: string }
        watch_query:          { type: string }
        watch_threshold:      { type: number }
        namespace_filter:     { type: string }
        enabled:              { type: boolean }
        consecutive_failures: { type: integer }
        secret:               { type: string, description: Only returned on creation. }
        created_at:           { type: string, format: date-time }
        updated_at:           { type: string, format: date-time }

    WebhookUpdateRequest:
      type: object
      properties:
        name:             { type: string }
        url:              { type: string }
        watch_query:      { type: string }
        watch_threshold:  { type: number }
        enabled:          { type: boolean }
        namespace_filter: { type: string, description: "Empty string clears the filter." }

paths:
  /v1/memories:
    post:
      operationId: storeMemory
      summary: Store a memory
      tags: [Memory]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StoreRequest" }
            example:
              content: "Customer prefers email, is on a budget"
              namespace: "customer:alice"
              tags: [preference]
      responses:
        "201":
          description: Memory created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StoreResponse" }
        "200":
          description: Memory deduplicated (existing memory updated)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StoreResponse" }
        "422": { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

    get:
      operationId: listMemories
      summary: List memories
      tags: [Memory]
      parameters:
        - { name: namespace,  in: query, schema: { type: string } }
        - { name: tags,       in: query, schema: { type: string }, description: Comma-separated }
        - { name: limit,      in: query, schema: { type: integer, default: 20, maximum: 100 } }
        - { name: offset,     in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: List of memories
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListResponse" }

    delete:
      operationId: deleteMemories
      summary: Delete memories (bulk)
      tags: [Memory]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DeleteRequest" }
            example: { namespace: "customer:alice" }
      responses:
        "200":
          description: Deleted count
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResponse" }

  /v1/memories/search:
    post:
      operationId: searchMemories
      summary: Semantic search
      tags: [Memory]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SearchRequest" }
            example:
              query: "how should I approach this customer?"
              namespace: "customer:alice"
              limit: 5
      responses:
        "200":
          description: Search results ordered by similarity score
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SearchResponse" }
        "503": { description: Embedding provider unavailable }

  /v1/memories/context:
    post:
      operationId: getContext
      summary: Build formatted context window
      tags: [Memory]
      description: >
        Semantic search + format in one call. Returns a string ready to inject
        into an LLM system prompt.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContextRequest" }
            example:
              query: "What does this customer need?"
              namespace: "customer:alice"
              limit: 5
              format: system_prompt
      responses:
        "200":
          description: Formatted context string
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContextResponse" }

  /v1/memories/export:
    get:
      operationId: exportMemories
      summary: Export memories as JSONL
      tags: [Memory]
      description: >
        Returns application/x-ndjson stream (one JSON object per line).
        Accepts same filters as GET /v1/memories. Hard limit: 10,000 rows.
      parameters:
        - { name: namespace,  in: query, schema: { type: string } }
        - { name: tags,       in: query, schema: { type: string } }
      responses:
        "200":
          description: JSONL stream
          content:
            application/x-ndjson:
              schema:
                type: string
                description: One JSON memory object per line

  /v1/workspace:
    get:
      operationId: getWorkspace
      summary: Get workspace info
      tags: [Workspace]
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkspaceInfo" }

  /v1/workspace/apikeys:
    post:
      operationId: createApiKey
      summary: Create API key
      tags: [Workspace]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 100 }
      responses:
        "201":
          content:
            application/json:
              schema:
                type: object
                properties:
                  key_id:     { type: string }
                  name:       { type: string }
                  api_key:    { type: string, description: Shown only once. }
                  created_at: { type: string, format: date-time }

  /v1/workspace/apikeys/{keyID}:
    delete:
      operationId: revokeApiKey
      summary: Revoke API key
      tags: [Workspace]
      parameters:
        - { name: keyID, in: path, required: true, schema: { type: string } }
      responses:
        "204": { description: Revoked }
        "404": { description: Key not found }

  /v1/webhooks:
    post:
      operationId: registerWebhook
      summary: Register a webhook
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookRegisterRequest" }
      responses:
        "201":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }

    get:
      operationId: listWebhooks
      summary: List webhooks
      tags: [Webhooks]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Webhook" }

  /v1/webhooks/{id}:
    get:
      operationId: getWebhook
      summary: Get webhook with recent deliveries
      tags: [Webhooks]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Webhook" }
                  - type: object
                    properties:
                      deliveries:
                        type: array
                        description: Last 20 deliveries

    put:
      operationId: updateWebhook
      summary: Update webhook
      tags: [Webhooks]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookUpdateRequest" }
      responses:
        "204": { description: Updated }

    delete:
      operationId: deleteWebhook
      summary: Delete webhook
      tags: [Webhooks]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "204": { description: Deleted }

  /v1/webhooks/{id}/test:
    post:
      operationId: testWebhook
      summary: Send test delivery
      tags: [Webhooks]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivered:   { type: boolean }
                  http_status: { type: integer }
                  error:       { type: string }

  /v1/webhooks/{id}/deliveries/{delivery_id}/retry:
    post:
      operationId: retryDelivery
      summary: Retry a failed delivery
      tags: [Webhooks]
      parameters:
        - { name: id,          in: path, required: true, schema: { type: string } }
        - { name: delivery_id, in: path, required: true, schema: { type: string } }
      responses:
        "202": { description: Retry enqueued }

  /health:
    get:
      operationId: healthCheck
      summary: Health check
      security: []
      responses:
        "200": { description: OK }
