Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/annsa-ai:annsa-ai-ingest-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Annsa Ingest API
version: '2026-08-22'
summary: Send customer feedback and call transcripts into Annsa, and reach its priorities and specs over MCP.
description: 'Annsa is the source of direction for your agents. Choose what to build from your customers, roadmap, ideas and agent findings, then direct every agent and learn as you go.
This document describes only what is public and callable today. Endpoints used by the Annsa web app on a user''s own session are deliberately absent.
## Rate limits
Limits are abuse ceilings, not metering — plan quotas are separate and are enforced on your account, not on the connection. `POST /feedback` and `POST /surface/submit` allow 5000 requests an hour; `POST /transcripts/upload` allows 500, because each call does real work. `POST /mcp` and `GET /health` carry no limit of their own.
A window is counted per authenticated caller, falling back to the client IP when there is no credential. Limited endpoints report where you stand in it on every response — `RateLimit`, plus the discrete `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` — so you never have to discover a limit by being refused. `RateLimit-Reset` is delta-seconds, not a timestamp. A 429 adds `Retry-After`. The full policy is at https://annsa.ai/api-policy.
## Which credential to use
There are two separate authentication systems, and they are not interchangeable:
- **`sessionBearer`** — a Supabase session token belonging to a signed-in user. Required by `POST /feedback` and `POST /transcripts/upload`. A survey key sent here returns 401.
- **`surveyKey`** — the key from Settings → Integrations → Survey. It authenticates `POST /surface/submit` **only**, and travels in the JSON body as `api_key`, not in a header — so it is described on the `WidgetSubmission` schema rather than as a security scheme, because OpenAPI has no way to express a body-field credential. This key is embedded in public web pages by design, so its reach is deliberately narrow.
For reading priorities, specs and search, use the MCP server rather than REST. It is OAuth 2.0 with PKCE and scoped `read`/`write`, and it is the surface built for agents. See `POST /mcp`.
## Errors
Every error response is `application/json` in the shape `{"detail": "..."}` (the `Error` schema). `detail` is a human-readable string; it is not a stable machine code, so branch on the HTTP status rather than on the text.
## Versioning
This API is unversioned in the path. The `info.version` above is the date this document was published, and this document is the contract. Additive changes — new optional fields, new endpoints — ship without notice, so clients must ignore unknown response fields. Breaking changes are announced at https://annsa.ai/changelog. A formal deprecation window with `Deprecation` and `Sunset` response headers is not yet in force; do not infer one from this document.'
contact:
name: Annsa
url: https://annsa.ai/contact
email: support@annsa.ai
license:
name: Proprietary
url: https://annsa.ai/terms
termsOfService: https://annsa.ai/terms
servers:
- url: https://api.annsa.ai
description: Production
security:
- sessionBearer: []
tags:
- name: Ingest
description: Getting customer signal into Annsa.
paths:
/feedback:
post:
operationId: submitFeedback
summary: Submit one piece of feedback, or a batch
description: 'Stores customer feedback and queues it for classification, clustering and ranking.
Send either a single item (a `text` field at the top level) or a batch (an `items` array). The two shapes return different bodies — see the responses below.
Annsa hashes the text, so the same content submitted twice creates one item. Supply `external_id` and the second submission is skipped on that id regardless of what the text says.
Requires a signed-in user''s session token. The survey key from Settings → Integrations → Survey does **not** authenticate this endpoint; use `POST /surface/submit` for that.
Rate limit: 5000 requests per hour.'
tags:
- Ingest
security:
- sessionBearer: []
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/FeedbackItem'
- $ref: '#/components/schemas/FeedbackBatch'
examples:
single:
summary: One piece of feedback
value:
text: The export is too slow on large datasets
customer_name: Jane Smith
customer_email: jane@example.com
revenue_band: 199
source: api
batch:
summary: Up to 1000 items in one request
value:
items:
- text: Can't find the export button
customer_email: alex@example.com
revenue_band: 49
- text: Love the new dashboard layout
customer_name: Sam Lee
responses:
'200':
description: Stored. A single submission returns the created id, or a duplicate marker if the text was already present. A batch returns one summary object covering every item.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/FeedbackAccepted'
- $ref: '#/components/schemas/FeedbackDuplicate'
- $ref: '#/components/schemas/FeedbackBatchResult'
examples:
accepted:
value:
id: 7c2f1a94-0d6e-4a1b-9f33-2b6f0c5a71de
duplicate:
value:
id: 7c2f1a94-0d6e-4a1b-9f33-2b6f0c5a71de
duplicate: true
original_source: slack
headers:
RateLimit:
$ref: '#/components/headers/RateLimit'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
Idempotent-Replayed:
$ref: '#/components/headers/IdempotentReplayed'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/IdempotencyConflict'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
parameters:
- $ref: '#/components/parameters/AnnsaVersion'
- $ref: '#/components/parameters/IdempotencyKey'
/transcripts/upload:
post:
operationId: uploadTranscript
summary: Upload a call transcript for segmentation
description: 'Accepts transcript text and returns immediately; segmentation into individual signals runs in the background.
`title` is required, which the prose guide does not currently say. `text` must be between 50 and 500,000 characters.
If the transcript carries speaker labels, name the customer''s label in `customer_speaker` so only their words become signals. Detect the available labels first with `POST /transcripts/detect-speakers`.
Requires a signed-in user''s session token. Rate limit: 500 requests per hour.'
tags:
- Ingest
security:
- sessionBearer: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TranscriptUpload'
examples:
discovery:
summary: A discovery call
value:
title: Acme — discovery call
text: 'Interviewer: What made you look for something like this?
Jane: We were tracking feature requests in a spreadsheet and it kept going stale...'
transcript_type: discovery
transcript_date: '2026-08-14'
company_name: Acme
customer_name: Jane Smith
customer_speaker: Jane
responses:
'200':
description: Accepted for processing.
content:
application/json:
schema:
$ref: '#/components/schemas/TranscriptAccepted'
headers:
RateLimit:
$ref: '#/components/headers/RateLimit'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
Idempotent-Replayed:
$ref: '#/components/headers/IdempotentReplayed'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/IdempotencyConflict'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
parameters:
- $ref: '#/components/parameters/AnnsaVersion'
- $ref: '#/components/parameters/IdempotencyKey'
/surface/submit:
post:
operationId: submitSurveyFeedback
summary: Submit feedback with a survey key
description: 'The endpoint the in-product survey widget posts to, and the only one a survey key authenticates.
The key travels in the request body as `api_key`, not in an `Authorization` header. That is deliberate: the key is embedded in public web pages, so it is scoped to this single write and nothing else. It cannot read anything, and it cannot reach `POST /feedback`.
Rate limit: 5000 requests per hour.'
tags:
- Ingest
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WidgetSubmission'
examples:
widget:
value:
api_key: your-survey-key
text: The export is too slow on large datasets
email: jane@example.com
feedback_type: bug
page_url: https://example.com/reports
user_metadata:
plan: pro
mrr: 199
responses:
'200':
description: Stored.
content:
application/json:
schema:
$ref: '#/components/schemas/FeedbackAccepted'
headers:
RateLimit:
$ref: '#/components/headers/RateLimit'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
'401':
description: The `api_key` was missing, unknown or revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
detail: Invalid API key
headers:
RateLimit:
$ref: '#/components/headers/RateLimit'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
parameters:
- $ref: '#/components/parameters/AnnsaVersion'
components:
headers:
IdempotentReplayed:
description: Present and `true` when this answer is the stored one for an `Idempotency-Key` already used, rather than a new write.
schema:
type: string
enum:
- 'true'
RateLimitRemaining:
description: Requests left in the current window.
schema:
type: integer
example: 4999
RateLimitReset:
description: Seconds until the window resets. Delta-seconds, per the IETF draft — not an absolute timestamp.
schema:
type: integer
example: 3599
RateLimit:
description: Your position in the current window, as the combined field from the later IETF drafts. Present on any response where the limit was actually evaluated.
schema:
type: string
example: limit=5000, remaining=4999, reset=3599
RateLimitLimit:
description: Requests permitted in the window.
schema:
type: integer
example: 5000
responses:
Unauthorized:
description: Missing, invalid or expired credential.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
detail: Authentication token is invalid or expired. Please log in again.
Unprocessable:
description: The body parsed but failed validation.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
detail: Invalid feedback format. Please check your data and try again.
BadRequest:
description: The request body was malformed or a required field was absent.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
detail: Either 'text' or 'items' field required
RateLimited:
description: Rate limit exceeded.
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
RateLimit:
$ref: '#/components/headers/RateLimit'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ServerError:
description: Unexpected server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
IdempotencyConflict:
description: A request with this Idempotency-Key is still running. Retry after the number of seconds in `Retry-After`.
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
example: 1
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
detail: A request with this Idempotency-Key is still running. Retry shortly.
parameters:
AnnsaVersion:
name: Annsa-Version
in: header
required: false
description: Pin the dated API contract. Omit it to use the current version. An unrecognised value is refused with 400 rather than downgraded. The version that served the request is echoed back in the same header. See https://annsa.ai/api-policy.
schema:
type: string
enum:
- '2026-08-22'
example: '2026-08-22'
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
description: 'Any unique string, up to 255 visible ASCII characters, to make a retry safe. The first request with a key runs, and its answer is kept for 24 hours for the credential that sent it. A retry with the same key and body gets that answer back, marked `Idempotent-Replayed: true`, without writing again. The same key with a different body is refused with 422, and a retry while the first is still running gets 409 with `Retry-After`. Leave it out and every request runs.'
schema:
type: string
minLength: 1
maxLength: 255
example: 7f3c2a9e-5b1d-4e8a-9c61-2d0f8b4a7e15
schemas:
TranscriptUpload:
type: object
title: TranscriptUpload
required:
- title
- text
properties:
title:
type: string
minLength: 1
maxLength: 500
text:
type: string
minLength: 50
maxLength: 500000
description: The transcript body. Speaker labels are preserved if present.
transcript_type:
type: string
default: discovery
description: What kind of conversation this was.
transcript_date:
type: string
description: When the conversation happened. Preserved on export.
company_name:
type: string
customer_name:
type: string
customer_email:
type: string
format: email
revenue_band:
type: string
sku:
type: string
project_id:
type: string
customer_speaker:
type: string
maxLength: 64
description: The customer's speaker label. Supply it and only their words become signals, so the interviewer's questions never reach the roadmap. Omit it and segmentation treats the whole transcript as one voice.
Error:
type: object
title: Error
description: The single error shape every endpoint returns. `detail` is human-readable and may change; branch on the HTTP status code instead.
required:
- detail
properties:
detail:
type: string
description: What went wrong, in plain language.
examples:
- Invalid API key
TranscriptAccepted:
type: object
title: TranscriptAccepted
description: Accepted; segmentation continues in the background.
properties:
id:
type: string
status:
type: string
additionalProperties: true
FeedbackBatch:
type: object
title: FeedbackBatch
description: Up to 1000 items in one request.
required:
- items
properties:
items:
type: array
minItems: 1
maxItems: 1000
items:
$ref: '#/components/schemas/FeedbackItem'
project_id:
type: string
description: Applies to every item in the batch.
FeedbackDuplicate:
type: object
title: FeedbackDuplicate
description: The text or external_id already existed, so nothing new was stored. The id points at the item already held.
required:
- id
- duplicate
properties:
id:
type: string
duplicate:
type: boolean
const: true
original_source:
type: string
description: Where the item already held came from.
examples:
- slack
FeedbackAccepted:
type: object
title: FeedbackAccepted
description: A single item was stored.
required:
- id
properties:
id:
type: string
description: The stored feedback's identifier.
FeedbackItem:
type: object
title: FeedbackItem
description: One piece of customer feedback.
required:
- text
properties:
text:
type: string
minLength: 1
description: The feedback content. The only required field.
customer_name:
type: string
description: Appears in Customer Voice quotes.
customer_email:
type: string
format: email
description: Enables close-the-loop notifications when the request ships.
revenue_band:
type:
- number
- string
description: What this customer pays you, e.g. 199. An amount is kept as an amount and never collapsed into a band. Legacy band strings (enterprise / paid / free) are still accepted.
examples:
- 199
sku:
type: string
description: Product tier or plan identifier.
external_id:
type: string
description: Your own identifier. A second submission carrying the same external_id is skipped, whatever the text says.
source:
type: string
description: Where this came from. Defaults to `api`. Setting it is what makes Activity Log filtering useful.
default: api
examples:
- api
- widget
- csv
- slack
project_id:
type: string
description: Home this feedback to a specific project.
additionalProperties: true
FeedbackBatchResult:
type: object
title: FeedbackBatchResult
description: The outcome of a batch submission, item by item.
required:
- batch
- total_items
- successful
- failed
properties:
batch:
type: boolean
const: true
total_items:
type: integer
successful:
type: integer
queued:
type: integer
description: Held against a plan limit rather than dropped, and released when capacity returns.
duplicates:
type: integer
failed:
type: integer
results:
type: array
items:
type: object
duplicates_list:
type: array
items:
type: object
errors:
type: array
items:
type: object
performance:
type: object
properties:
total_time_seconds:
type: number
items_per_second:
type: number
WidgetSubmission:
type: object
title: WidgetSubmission
description: A submission from the in-product survey. Note that the credential is the `api_key` field below, not a header.
required:
- api_key
properties:
api_key:
type: string
description: The survey key from Settings → Integrations → Survey. This is the credential for this endpoint.
text:
type: string
description: The feedback content.
email:
type: string
maxLength: 255
feedback_type:
type: string
enum:
- bug
- feature
- question
- other
page_url:
type: string
maxLength: 2000
description: Where the feedback was given.
screenshot:
type: string
description: Base64-encoded image.
surface:
type: string
enum:
- float
- inline
- bar
- thumbs
- page
- widget
description: Which collection surface captured this.
rating:
type: integer
user_metadata:
type: object
description: Who the submitter is. Recognised keys include `email`, `name`, `full_name`, `displayName`, `id`, `plan`, `tier`, and any of `amount` / `mrr` / `arr` / `revenue` for what they pay.
additionalProperties: true
securitySchemes:
sessionBearer:
type: http
scheme: bearer
bearerFormat: JWT
description: A Supabase session token for a signed-in user. Grants that user's own workspace scope and nothing wider. This is not the survey key — sending a survey key here returns 401.
oauth2:
type: oauth2
description: OAuth 2.0 for the MCP resource `https://app.annsa.ai/mcp`. PKCE with S256 is required. Clients may register themselves dynamically (RFC 7591); server metadata is published per RFC 8414 at `/.well-known/oauth-authorization-server` and protected-resource metadata per RFC 9728 at `/.well-known/oauth-protected-resource/mcp`.
flows:
authorizationCode:
authorizationUrl: https://app.annsa.ai/mcp/auth
tokenUrl: https://app.annsa.ai/mcp/token
refreshUrl: https://app.annsa.ai/mcp/token
scopes:
read: Read priorities, specs, survey results and search.
write: Change state — start building, mark shipped, share back with customers.
externalDocs:
description: 'Versioning, deprecation and rate limits: how the version header works, what counts as a breaking change, the 90 days of notice before anything is removed, and the ceilings each endpoint carries.'
url: https://annsa.ai/api-policy