Endpoints
Feedback
Report a bad result on any endpoint — the closed tag vocabulary, the correction that matters most, and what we do with it.
/v1/feedbackReport a problem with a search or one of its results
Scopes (any one): dsp.search.read, dsp.usage.read
Use it when a result is wrong, stale, or missing in a way only the caller can see. Outcome: a report attached to the exact run and result set on our side. The correction you supply ("what should have come back") feeds directly into index and ranker fixes.
The index is large and the ranker has opinions, so the failures worth fixing are the ones only the caller can see: the right kind of company but the wrong one, a record that was true last year, an obvious name that never came back. None of that is visible to us from a log line. This route is how you tell us.
- Every endpoint that returns results: search,
/v1/similar,/v1/entities/*,/v1/relationships, and agent runs. One endpoint rather than one per surface, because everything about a report except finding the subject is identical. - General problems too. With
context: "help", a report can be about the product rather than one result. The dashboard's Help menu files its problem reports this way. - Free. It charges no credits, and it is open to any credential that can
search: an API key with
dsp.search.readordsp.usage.read, or a dashboard session. - Private. Reports are private to the credential that filed them.
Request#
commentstring | nullFree text. Deliberately unbounded.
contextstring | null'help' for a general problem report from the dashboard's Help menu — about the product, not one search, so subject_ref may be omitted and target.kind must be 'result_set'.
expectedExpectedCorrection | nullnotestring | nullshould_excludearray of string, ≤ 200should_includearray of string, ≤ 200sentiment"bad" | "good"default: badsubject_refSubjectRef | nullThe search or run reported. Required unless context is 'help'.
idstringrequiredmeta.request_id for 'request'; run_id for 'agent_run'. ≥ 1 chars, ≤ 128 chars.
kind"request" | "agent_run"required'request' for anything in your usage history — a search, a look-alike run, an entity lookup, a relationship query — identified by its meta.request_id. 'agent_run' for an agentic run, identified by its run_id.
tagsarray of string, ≤ 10targetFeedbackTargetrequiredentity_idstring | null≤ 128 chars.
entity_typestring | null≤ 32 chars.
kind"result" | "result_set"requiredThree parts carry the meaning: subject_ref, target and expected. A
fourth, context, is only for problem reports with no subject.
subject_ref: what you are reporting
It is named after the id it carries, not after the page you were on. There are two kinds:
kind | id | Covers |
|---|---|---|
request | meta.request_id from the response | Search, /v1/similar, /v1/entities/*, /v1/relationships |
agent_run | run_id from POST /v1/agents/runs | Agent runs |
There is deliberately no value per endpoint. Which endpoint a request came from
is already recorded in your usage history
as action_type, so asking you to declare it again would create a second source
of truth for one fact, and no way to reconcile the two when they disagree.
- A
requestmust be one this credential made, and one still inside the 90-day history window; anything else answers404 feedback.subject_not_found. How much you have run since does not matter: the lookup is keyed onrequest_id, so a search from ten weeks and a million calls ago reports exactly like one from a minute ago. - An
agent_runis scoped to your tenant rather than to the credential: the same visibilityGET /v1/agents/runs/{run_id}already gives, because agent runs record no user id. Anyone on your team who can fetch the run can report it.
Apart from a help report (below), every report needs a subject_ref, and
nothing else identifies the subject, so keep the id if you intend to report
asynchronously.
context: "help": a problem with no subject
Some problems are not about one search or run: a page that will not load, a
filter that behaves oddly everywhere. Send context: "help" to report one.
subject_refmay be omitted. If you do send one, it is checked like any other subject.target.kindmust beresult_set, because the report is not about one record.- The report needs a
commentor at least one tag saying what went wrong.
{
"context": "help",
"target": { "kind": "result_set" },
"tags": ["other"],
"comment": "The filters panel does not open on the search page."
}A report filed without a subject is listed with subject_kind: "help".
target.kind: which level you are complaining about
They are different claims, and the tag vocabulary is split accordingly:
result: this one record is wrong.entity_idis required, and is theidfrom the row as it was returned.result_set: the page is wrong (bad order, something missing, nothing came back).entity_idmust be omitted.
expected: the field we act on fastest
A tag says something was wrong; should_include / should_exclude say what
right looks like, and only the second can be checked against the index.
tags must come from the closed vocabulary below; an unknown tag is a 422
naming the valid set. comment is free text and deliberately unbounded.
The tag vocabulary#
curl "https://api.sproutsdata.ai/v1/feedback/tags" \
-H "X-API-Key: $SPROUTS_API_KEY"tagsarray of TagDescriptorapplies_tostringdescriptionstringlabelstringrequires_commentbooleandefault: falsetagstringversionintegerapplies_to tells you which target.kind a tag belongs to, and it is checked
on write. A set-level tag filed against a single result is a 422 naming the
tag and listing what is legal at that level — not a silent reinterpret, and not
a stored claim about the wrong thing.
| Tag | Applies to | Label | Means |
|---|---|---|---|
irrelevant | result | Not what I asked for | This has nothing to do with what I searched for |
wrong_entity | result | Wrong company or person | The right kind of result, but not the one I meant — like asking for lenders and getting a payments company |
stale | result | Out of date | This was true once, but it is not true now |
data_quality | result | Details are wrong | The right company or person, but something on the record is incorrect |
duplicate | result | Listed more than once | The same company or person appears again elsewhere in these results |
bad_ranking | result_set | Wrong order | The right results, but the best ones are not near the top |
missing | result_set | Something is missing | A company or person I expected to see is not here |
no_results | result_set | Nothing came back | This returned an empty list |
too_few | result_set | Too few results | There should be more of these than came back |
too_many | result_set | Too broad | Far more results than what I asked for |
other | any | Something else | None of these fit — requires a comment |
label is the short line a form should show; description is the clarifier
under it. Both come from the endpoint so that every client says the same thing.
other, and the one condition on it
A closed list with no escape hatch does not stop you having a problem it fails
to describe. It makes you pick the nearest wrong tag, which quietly corrupts
the counts we act on. other exists so you never have to.
- It is the one tag legal at both levels (
applies_to: "any"). - It is the one that requires a
comment: "something else" with nothing written beside it says only that something was wrong, which the report itself already said. Omitting the comment is a422. - Every tag publishes
requires_comment, so a client enforces this without hardcoding which tag is special. That set will grow.
other is also how this vocabulary grows. When the same complaint keeps
arriving as other plus a paragraph, that is the evidence for promoting it to a
tag of its own.
The table is a convenience for reading; the endpoint is the contract.
Response#
created_atstringfeedback_idstringtag_vocab_versionintegerdefault: 2target_verifiedboolean201 with the stored id. Two fields deserve a note:
target_verified— whether we could confirm the record you named was in that result set.falseis not a challenge to your report: it means we could not check — the run is past its 30-day snapshot retention, its page was too large to keep, or the subject is anagent_run, whose rows we do not hold on this side. The report is recorded either way.tag_vocab_version— the vocabulary your tags were interpreted under.
Reading your reports back#
curl "https://api.sproutsdata.ai/v1/feedback?subject_id=req_01JB2XKQ9F8ZP4M7&limit=50" \
-H "X-API-Key: $SPROUTS_API_KEY"subject_idstring | null≤ 128 chars.
statusstring | null≤ 16 chars.
tagstring | null≤ 32 chars.
subject_kindstring | null≤ 32 chars.
limitintegerdefault: 50≥ 1, ≤ 200.
offsetintegerdefault: 0≥ 0.
Your credential's own reports, newest first. An API key sees what that key
filed; a session sees the signed-in user's. Pass subject_id to narrow to one
search.
- Narrow with
tag,statusandsubject_kind(request,agent_runorhelp). They compose, andtotalnarrows with them. A value outside the published vocabulary is a422naming the valid set, not an empty list: "you never reported that" and "that is not a tag" are different answers, and a typo should not read like the first one. - Page with
limitandoffset. The response carriestotalbeside the rows (how many reports you have filed under this credential, not how many came back), so you can tell a full page from the last one without asking for the next. subjecton each row is the run it was about: what you searched for, on which endpoint, and what came back. It isnullwhen there is no run to show: ahelpreport with no subject, a run past the 90-day history, or anagent_run. It is nevernullbecause anything is wrong with the report.
limitintegerdefault: 0offsetintegerdefault: 0resultsarray of FeedbackViewcommentstring | nullcreated_atstringexpectedobject | nullfeedback_idstringsentimentstringstatusstringsubjectFeedbackSubject | nullaction_typestringoccurred_atstringquerystring | nullresult_countinteger | nullresult_totalinteger | nullresult_total_is_estimateboolean | nullrun_idinteger | nullstatusstringsubject_idstringsubject_kindstringtag_vocab_versionintegertagsarray of stringtarget_entity_idstring | nulltarget_kindstringtarget_verifiedbooleantotalintegerdefault: 0Votes: thumbs and flags#
A report is the detailed route. A vote is the one-tap one: thumbs up or down on
a whole result set, or a flag on one record. Votes are free, and they take the
same credentials as reports (dsp.search.read or dsp.usage.read, or a
dashboard session).
curl -X PUT "https://api.sproutsdata.ai/v1/feedback/votes" \
-H "X-API-Key: $SPROUTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"request_id": "req_01JB2XKQ9F8ZP4M7",
"level": "result_set",
"entity_category": "company",
"vote": "dislike",
"reason_code": "missing_expected",
"text": "Gusto and Rippling should be here."
}'entity_categoryEntityCategoryrequiredThe category of the results (result_set) or of the record (result).
entity_idstring | nullThe flagged record's id. Required for result, refused for result_set. ≥ 1 chars, ≤ 255 chars.
level"result_set" | "result"requiredresult_set — thumbs on the whole set; result — flag one record.
reason_codestring | nullOptional reason chip. result_set: not_relevant | missing_expected | too_few | wrong_or_outdated | other. result: wrong_entity | outdated | duplicate | missing_info | other. Never sent with like. ≤ 32 chars.
request_idstringrequiredmeta.request_id of the response the vote is about (an agent run's run_id for Agent results). ≥ 1 chars, ≤ 128 chars.
textstring | nullOptional free text, only after a reason. Stored here only — never sent to analytics. ≤ 2000 chars.
vote"like" | "dislike" | "flag"requiredlike/dislike on a result set; flag on a single result.
The rules a vote must follow:
level | vote | entity_id |
|---|---|---|
result_set | like or dislike | Must be omitted |
result | flag | Required: the id of the record, as it was returned |
request_idis themeta.request_idof the response you are voting on, or therun_idfor an agent run's results.entity_categoryis the category of the results (result_set) or of the flagged record (result).reason_codeis optional, and comes from the level's own vocabulary below. Aliketakes no reason and no text.textis optional free text, up to 2,000 characters, and is accepted only together with areason_code.
A vote that breaks one of these rules is a 422 that says which.
Reason codes
The vote vocabulary is separate from the report tags.
level | reason_code values |
|---|---|
result_set | not_relevant, missing_expected, too_few, wrong_or_outdated, other |
result | wrong_entity, outdated, duplicate, missing_info, other |
One active vote, changeable
changedbooleanFalse when the same vote was already active — nothing was stored.
voteVoteViewThe caller's own vote. text is theirs, so it is echoed back to them.
created_atstringentity_categorystringentity_idstring | nulllevel"result_set" | "result"reason_codestring | nullrequest_idstringtextstring | nullvote"like" | "dislike" | "flag"vote_idstring- You hold one active vote per subject: per
request_id,leveland, for a flag,entity_id. Sending a different vote replaces it. - Sending the same vote again (same
vote,reason_codeandtext) changes nothing and returnschanged: false, so a double tap is harmless. - "You" is the credential: an API key's votes are that key's, and a session's are the signed-in user's.
- A vote is not checked against your usage history, so a vote on a run past its retention is still recorded.
- There is no route to withdraw a vote. Send a different one to change it.
Reading your votes back
curl "https://api.sproutsdata.ai/v1/feedback/votes?request_id=req_01JB2XKQ9F8ZP4M7" \
-H "X-API-Key: $SPROUTS_API_KEY"request_idstringrequired≥ 1 chars, ≤ 128 chars.
Your active votes on one response, oldest first. Replaced votes are not included.
request_idstringvotesarray of VoteViewcreated_atstringentity_categorystringentity_idstring | nulllevel"result_set" | "result"reason_codestring | nullrequest_idstringtextstring | nullvote"like" | "dislike" | "flag"vote_idstringIf a vote cannot be stored or read, the answer is 503 feedback.unavailable
and nothing was saved. Send it again.
What happens next#
Reports are not a support ticket queue and this is not a promise of a reply. They land in an evaluation set:
- A report against a
requestjoins that run's stored request, resolved plan, and page-1 snapshot, the same three things/v1/usage/history/{run_id}shows you, so the failure can be replayed and re-scored without re-running anything. expectedentries become the check: did the record you named exist in the index, was it reachable under the filters that were applied, and where did it rank.
That is what turns a complaint into a fix.
Errors#
| Status | Code | Summary |
|---|---|---|
| 401 | auth.unauthorized | No usable credential (cause deliberately unspecified) |
| 403 | auth.browser_origin | A secret key was used from a browser |
| 403 | auth.insufficient_scope | The credential does not hold the required scope |
| 404 | feedback.subject_not_found | The search being reported is not one this credential ran |
| 422 | pagination.cursor_invalid | The cursor could not be read (e.g. a filter changed mid-pagination) |
| 422 | request.invalid | A field failed validation (extra.errors has the detail) |
| 503 | credits.unavailable | The billing service could not confirm the charge; retry with the same Idempotency-Key |
| 503 | upstream.unavailable | The retrieval engine is unreachable (not charged) |