{
  "openapi": "3.1.0",
  "info": {
    "title": "Online Legacy API",
    "description": "Public API for Online Legacy — AI memory preservation platform. Create and interact with conversational virtual twins of loved ones.\n\n## Versioning\n\nThis API uses URL path versioning. The current stable version is **v1**, accessible at `/api/v1/`. The unversioned paths (`/ask`, `/api/requests`, `/api/legacy/{slug}/chat`) remain supported as stable aliases for v1 endpoints.\n\nDeprecation is communicated via `Sunset` and `Deprecation` response headers at least 6 months before removal. A `Link` header pointing to the successor endpoint will accompany any deprecated response.\n\n## Idempotency\n\nAll write operations accept an `Idempotency-Key` request header (or an `idempotencyKey` field in the request body). Submit the same key with identical content to receive the original response without creating a duplicate record. Keys expire after 24 hours.\n\n## Rate limits\n\nRate limit state is returned on every write response via `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `RateLimit-Policy` headers. A `429 Too Many Requests` response includes `Retry-After` in seconds.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Pass `cursor` (opaque string from a previous response's `nextCursor` field) and `limit` (1–100, default 20) as query parameters. The response shape is `{ items: [...], nextCursor: string | null }`.\n\n## Sandbox\n\nAppend `?sandbox=true` to any write endpoint to exercise the full request lifecycle — validation, idempotency, and response shape — without writing to the production database. Sandbox responses are identical in shape to production responses and include an `X-Sandbox: true` response header.",
    "version": "1.0.0",
    "x-api-version": "v1",
    "x-deprecation-policy": "Endpoints are deprecated via Sunset and Deprecation response headers with at least 6 months notice. Breaking changes are introduced only in new major versions.",
    "contact": {
      "url": "https://online-legacy.com/contact"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://online-legacy.com",
      "description": "Production"
    },
    {
      "url": "https://online-legacy.com",
      "description": "Sandbox — append ?sandbox=true to any write endpoint to run without persisting data",
      "x-sandbox": true
    }
  ],
  "tags": [
    { "name": "nlweb", "description": "Natural language query interface (NLWeb v0.55)" },
    { "name": "legacy", "description": "Legacy profile interactions" },
    { "name": "requests", "description": "Legacy creation requests" },
    { "name": "batch", "description": "Batch operations" }
  ],
  "paths": {
    "/ask": {
      "post": {
        "operationId": "nlweb_query_post",
        "summary": "Natural language query",
        "description": "Submit a natural language question about Online Legacy. Returns a structured NLWeb v0.55 answer. Pass `prefer.streaming: true` to receive a Server-Sent Events stream.",
        "tags": ["nlweb"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/NLWebRequest" },
              "examples": {
                "simple": {
                  "summary": "Simple question",
                  "value": { "query": { "text": "How does Online Legacy work?" } }
                },
                "streaming": {
                  "summary": "Streaming question",
                  "value": { "query": { "text": "What is a digital legacy?" }, "prefer": { "streaming": true } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful answer",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NLWebResponse" }
              },
              "text/event-stream": {
                "description": "SSE stream when prefer.streaming is true",
                "schema": { "type": "string" }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      },
      "get": {
        "operationId": "nlweb_query_get",
        "summary": "Natural language query (GET)",
        "description": "Submit a natural language question via query parameter.",
        "tags": ["nlweb"],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The natural language question",
            "schema": { "type": "string", "example": "How does Online Legacy work?" }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful answer",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/NLWebResponse" } }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/requests": {
      "post": {
        "operationId": "create_legacy_request",
        "summary": "Submit a legacy creation request",
        "description": "Submit a request to create an AI virtual twin for a loved one. An Online Legacy team member will follow up to begin the intake process.\n\nSupports idempotency via the `Idempotency-Key` header or `idempotencyKey` body field. Re-submitting with the same key and identical content returns the original response without creating a duplicate.\n\nAppend `?sandbox=true` to test the full request lifecycle without persisting data.",
        "tags": ["requests"],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" },
          { "$ref": "#/components/parameters/SandboxMode" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LegacyRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Request submitted",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true }
                  },
                  "required": ["ok"]
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "409": {
            "description": "Idempotency conflict — same key submitted with different content",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "422": {
            "description": "Unprocessable entity — field validation failed",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } }
            }
          },
          "429": {
            "description": "Too many requests",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/requests/batch": {
      "post": {
        "operationId": "batch_legacy_requests",
        "summary": "Submit multiple legacy creation requests",
        "description": "Submit up to 10 legacy creation requests in a single call. Each item is processed independently — a failure in one item does not affect others. Returns a result array in the same order as the input.\n\nThis endpoint is useful for agents acting on behalf of multiple families at once.",
        "tags": ["batch"],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["requests"],
                "properties": {
                  "requests": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": { "$ref": "#/components/schemas/LegacyRequest" },
                    "description": "Array of legacy creation requests (1–10)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "207": {
            "description": "Multi-status — partial success possible",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid batch structure",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "429": {
            "description": "Too many requests",
            "headers": {
              "Retry-After": { "schema": { "type": "integer" } }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/legacy/{slug}/chat": {
      "post": {
        "operationId": "legacy_chat",
        "summary": "Chat with a legacy profile",
        "description": "Send a message to a published AI legacy profile and receive a streamed response in the voice and knowledge of the person.",
        "tags": ["legacy"],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Legacy profile slug (e.g. 'margaret')",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": { "type": "string", "description": "User message" },
                  "sessionId": { "type": "string", "description": "Optional conversation session ID for continuity" }
                },
                "required": ["message"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Streamed response",
            "content": {
              "text/event-stream": { "schema": { "type": "string" } }
            }
          },
          "404": {
            "description": "Profile not found",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{jobId}": {
      "get": {
        "operationId": "get_job_status",
        "summary": "Poll an async job",
        "description": "Retrieve the current status of a long-running async job. Jobs are created by operations that return `202 Accepted`. Poll this endpoint until `status` is `completed` or `failed`.",
        "tags": ["legacy"],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "description": "The job ID returned in the 202 response",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status",
            "headers": {
              "Retry-After": {
                "description": "Suggested seconds before next poll (only when status is pending or processing)",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobStatus" }
              }
            }
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Client-generated unique key (8–128 chars) to make this request idempotent. Re-submitting with the same key and identical content returns the cached response without creating a duplicate.",
        "schema": { "type": "string", "minLength": 8, "maxLength": 128 }
      },
      "SandboxMode": {
        "name": "sandbox",
        "in": "query",
        "required": false,
        "description": "Set to `true` to exercise the full request lifecycle without persisting data. Sandbox responses are identical in shape to production and include `X-Sandbox: true`.",
        "schema": { "type": "boolean" }
      },
      "CursorParam": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque cursor from a previous response's `nextCursor` field. Omit to start from the beginning.",
        "schema": { "type": "string" }
      },
      "LimitParam": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of items to return (1–100, default 20).",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "The maximum number of requests allowed in the current window",
        "schema": { "type": "integer" }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window",
        "schema": { "type": "integer" }
      },
      "RateLimitReset": {
        "description": "Unix timestamp (seconds) when the window resets",
        "schema": { "type": "integer" }
      },
      "RateLimitPolicy": {
        "description": "Rate limit policy string per RFC draft-ietf-httpapi-ratelimit-headers",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "NLWebRequest": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {
            "type": "object",
            "required": ["text"],
            "properties": {
              "text": { "type": "string", "description": "The natural language question" }
            }
          },
          "prefer": {
            "type": "object",
            "properties": {
              "streaming": { "type": "boolean", "description": "If true, return SSE stream" }
            }
          }
        }
      },
      "NLWebResponse": {
        "type": "object",
        "properties": {
          "_meta": {
            "type": "object",
            "properties": {
              "response_type": { "type": "string", "enum": ["answer", "list"] },
              "version": { "type": "string", "example": "0.55" }
            }
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "@type": { "type": "string", "example": "Answer" },
                "text": { "type": "string" },
                "url": { "type": "string", "format": "uri" }
              }
            }
          }
        }
      },
      "LegacyRequest": {
        "type": "object",
        "required": ["fullName", "email", "phone"],
        "properties": {
          "fullName": { "type": "string", "description": "Requester's full name", "maxLength": 200 },
          "email": { "type": "string", "format": "email", "description": "Contact email", "maxLength": 320 },
          "phone": { "type": "string", "description": "Contact phone number", "maxLength": 40 },
          "subjectName": { "type": "string", "description": "Name of the person to preserve" },
          "relationship": { "type": "string", "description": "Relationship to the subject" },
          "message": { "type": "string", "description": "Additional context or notes" },
          "idempotencyKey": { "type": "string", "minLength": 8, "maxLength": 128, "description": "Idempotency key (alternative to the Idempotency-Key header)" }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": ["results"],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["status"],
              "properties": {
                "status": { "type": "integer", "description": "HTTP status for this item (201, 409, 422, etc.)" },
                "ok": { "type": "boolean" },
                "error": { "type": "string" },
                "fields": { "type": "object", "additionalProperties": { "type": "string" } }
              }
            }
          }
        }
      },
      "JobStatus": {
        "type": "object",
        "required": ["jobId", "status"],
        "properties": {
          "jobId": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["pending", "processing", "completed", "failed"],
            "description": "Current job status"
          },
          "result": { "type": "object", "description": "Present when status is completed" },
          "error": { "type": "string", "description": "Present when status is failed" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PaginatedResponse": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": { "type": "array", "items": {} },
          "nextCursor": { "type": "string", "nullable": true, "description": "Pass as `cursor` in the next request. Null when there are no more pages." },
          "totalCount": { "type": "integer", "description": "Total number of matching items (optional)" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message" },
          "code": { "type": "string", "description": "Machine-readable error code" },
          "retryAfter": { "type": "integer", "description": "Seconds to wait before retrying (rate limit or service unavailable responses)" }
        }
      },
      "ValidationError": {
        "type": "object",
        "required": ["error", "fields"],
        "properties": {
          "error": { "type": "string", "example": "Validation failed" },
          "fields": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Map of field name to error message"
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://online-legacy.com/developers"
  }
}
