{
  "openapi": "3.1.0",
  "info": {
    "title": "Sentriment API",
    "version": "1.0.0",
    "description": "Ingest qualitative feedback and read AI-generated clusters. Authenticate with `Authorization: Bearer sk_…` (server) or `pk_…` (widget; origin-checked). Errors follow RFC 7807 (application/problem+json)."
  },
  "servers": [
    {
      "url": "https://app.sentriment.com"
    }
  ],
  "paths": {
    "/api/v1/feedback": {
      "get": {
        "summary": "List feedback items (newest first)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cluster",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sentiment",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "positive",
                "neutral",
                "negative"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "api",
                "widget",
                "csv",
                "appstore",
                "intercom",
                "zendesk"
              ]
            }
          },
          {
            "name": "meta",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Filter on custom properties as key:value (e.g. test:paywall-v2); repeat to AND several."
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Cursor from nextBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "Items + nextBefore cursor"
          }
        }
      },
      "post": {
        "summary": "Submit a feedback item",
        "description": "Returns 202 immediately; processing (redaction beyond the built-in pass, embedding, clustering, sentiment) is asynchronous. Supply an `Idempotency-Key` header to make retries safe.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 10000
                  },
                  "userId": {
                    "type": "string",
                    "maxLength": 256
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 320
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "api",
                      "widget"
                    ]
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {}
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  }
                },
                "required": [
                  "text"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate Idempotency-Key; original item returned"
          },
          "202": {
            "description": "Accepted for processing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (problem+json)"
          },
          "401": {
            "description": "Missing/invalid key (problem+json)"
          },
          "403": {
            "description": "Origin not allowed for widget key (problem+json)"
          },
          "429": {
            "description": "Rate limited (problem+json, Retry-After header)"
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "summary": "Semantic search over feedback",
        "description": "Embeds the query and ranks feedback by meaning (cosine similarity), not keywords. 60 requests/min per key.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 500
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked results with similarity scores"
          },
          "400": {
            "description": "Missing/invalid q (problem+json)"
          },
          "429": {
            "description": "Rate limited (problem+json)"
          }
        }
      }
    },
    "/api/v1/users/identify": {
      "post": {
        "summary": "Attach traits to an end user (Mixpanel-style identify)",
        "description": "Shallow-merges traits (max 32 keys, 8KB). Accepts secret keys, or widget keys from an allowed origin. Traits power segment filters (?trait=plan:pro) and cluster segment breakdowns. Outcome traits (e.g. churned: \"true\", mrr) unlock impact correlation: every theme and Ask answer shows how the users behind it churn and how much revenue is at stake — traits merge, so flagging a churn after the fact retroactively correlates their old feedback.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId",
                  "traits"
                ],
                "properties": {
                  "userId": {
                    "type": "string",
                    "maxLength": 256
                  },
                  "traits": {
                    "type": "object",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Merged traits returned"
          },
          "429": {
            "description": "Rate limited (120/min per key)"
          }
        }
      }
    },
    "/api/v1/users/{externalUserId}": {
      "get": {
        "summary": "Fetch one end user (GDPR Art. 15 access)",
        "description": "Profile traits plus the feedback we hold for this user (up to 200 items).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "externalUserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile + feedback"
          },
          "404": {
            "description": "Unknown user"
          }
        }
      },
      "delete": {
        "summary": "Delete all feedback from one end user (GDPR Art. 17)",
        "description": "Queues a hard delete of every feedback item with this userId. Returns 202 with a request id.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "externalUserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Deletion queued"
          }
        }
      }
    },
    "/api/v1/ping": {
      "get": {
        "summary": "Validate a key / connectivity check",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Key is valid; returns project + key type"
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "summary": "List webhook endpoints",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoints with delivery health"
          }
        }
      },
      "post": {
        "summary": "Create a webhook endpoint",
        "description": "Events: feedback.processed, cluster.spiking, cluster.regressed, cluster.resolved, user.health_changed, question.verdict_changed. The signing secret is returned exactly once. Deliveries carry X-Sentriment-Signature: t=<unix>,v1=<hex hmac_sha256(secret, `${t}.${body}`)>; retried 5x with backoff; endpoint auto-disabled after 10 consecutive failures.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "feedback.processed",
                        "cluster.spiking",
                        "cluster.regressed",
                        "cluster.resolved",
                        "user.health_changed",
                        "question.verdict_changed"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; includes the one-time secret"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "patch": {
        "summary": "Pause or resume a webhook endpoint",
        "description": "Set active=false to stop deliveries without losing the endpoint or its signing secret. Set active=true to resume, which also resets the consecutive-failure counter — this is how an endpoint auto-disabled after 10 failures is brought back.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "active"
                ],
                "properties": {
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated endpoint"
          },
          "404": {
            "description": "No such endpoint in this project"
          }
        }
      },
      "delete": {
        "summary": "Delete a webhook endpoint",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          }
        }
      }
    },
    "/api/v1/clusters/{id}": {
      "get": {
        "summary": "Cluster detail with recent items",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cluster + items"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/feedback/{id}": {
      "delete": {
        "summary": "Delete one feedback item",
        "description": "Hard delete; cluster counters are recomputed. Returns 204.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "get": {
        "summary": "Fetch one feedback item with analysis results",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback item"
          },
          "404": {
            "description": "Not found (problem+json)"
          }
        }
      }
    },
    "/api/v1/clusters": {
      "get": {
        "summary": "List active clusters for the project",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cluster list"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  }
}
