Endpoints

Feedback

Report a bad result on any endpoint — the closed tag vocabulary, the correction that matters most, and what we do with it.

POST/v1/feedback

Report 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.read or dsp.usage.read, or a dashboard session.
  • Private. Reports are private to the credential that filed them.

Request#

commentstring | null

Free 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 | null
notestring | null
should_excludearray of string, ≤ 200
should_includearray of string, ≤ 200
sentiment"bad" | "good"default: bad
subject_refSubjectRef | null

The search or run reported. Required unless context is 'help'.

idstringrequired

meta.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, ≤ 10
targetFeedbackTargetrequired
entity_idstring | null

≤ 128 chars.

entity_typestring | null

≤ 32 chars.

kind"result" | "result_set"required

Three 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:

kindidCovers
requestmeta.request_id from the responseSearch, /v1/similar, /v1/entities/*, /v1/relationships
agent_runrun_id from POST /v1/agents/runsAgent 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 request must be one this credential made, and one still inside the 90-day history window; anything else answers 404 feedback.subject_not_found. How much you have run since does not matter: the lookup is keyed on request_id, so a search from ten weeks and a million calls ago reports exactly like one from a minute ago.
  • An agent_run is scoped to your tenant rather than to the credential: the same visibility GET /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_ref may be omitted. If you do send one, it is checked like any other subject.
  • target.kind must be result_set, because the report is not about one record.
  • The report needs a comment or 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_id is required, and is the id from the row as it was returned.
  • result_set: the page is wrong (bad order, something missing, nothing came back). entity_id must 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 TagDescriptor
applies_tostring
descriptionstring
labelstring
requires_commentbooleandefault: false
tagstring
versioninteger

applies_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.

TagApplies toLabelMeans
irrelevantresultNot what I asked forThis has nothing to do with what I searched for
wrong_entityresultWrong company or personThe right kind of result, but not the one I meant — like asking for lenders and getting a payments company
staleresultOut of dateThis was true once, but it is not true now
data_qualityresultDetails are wrongThe right company or person, but something on the record is incorrect
duplicateresultListed more than onceThe same company or person appears again elsewhere in these results
bad_rankingresult_setWrong orderThe right results, but the best ones are not near the top
missingresult_setSomething is missingA company or person I expected to see is not here
no_resultsresult_setNothing came backThis returned an empty list
too_fewresult_setToo few resultsThere should be more of these than came back
too_manyresult_setToo broadFar more results than what I asked for
otheranySomething elseNone 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 a 422.
  • 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_atstring
feedback_idstring
tag_vocab_versionintegerdefault: 2
target_verifiedboolean

201 with the stored id. Two fields deserve a note:

  • target_verified — whether we could confirm the record you named was in that result set. false is 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 an agent_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, status and subject_kind (request, agent_run or help). They compose, and total narrows with them. A value outside the published vocabulary is a 422 naming 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 limit and offset. The response carries total beside 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.
  • subject on each row is the run it was about: what you searched for, on which endpoint, and what came back. It is null when there is no run to show: a help report with no subject, a run past the 90-day history, or an agent_run. It is never null because anything is wrong with the report.
limitintegerdefault: 0
offsetintegerdefault: 0
resultsarray of FeedbackView
commentstring | null
created_atstring
expectedobject | null
feedback_idstring
sentimentstring
statusstring
subjectFeedbackSubject | null
action_typestring
occurred_atstring
querystring | null
result_countinteger | null
result_totalinteger | null
result_total_is_estimateboolean | null
run_idinteger | null
statusstring
subject_idstring
subject_kindstring
tag_vocab_versioninteger
tagsarray of string
target_entity_idstring | null
target_kindstring
target_verifiedboolean
totalintegerdefault: 0

Votes: 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_categoryEntityCategoryrequired

The category of the results (result_set) or of the record (result).

entity_idstring | null

The flagged record's id. Required for result, refused for result_set. ≥ 1 chars, ≤ 255 chars.

level"result_set" | "result"required

result_set — thumbs on the whole set; result — flag one record.

reason_codestring | null

Optional 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_idstringrequired

meta.request_id of the response the vote is about (an agent run's run_id for Agent results). ≥ 1 chars, ≤ 128 chars.

textstring | null

Optional free text, only after a reason. Stored here only — never sent to analytics. ≤ 2000 chars.

vote"like" | "dislike" | "flag"required

like/dislike on a result set; flag on a single result.

The rules a vote must follow:

levelvoteentity_id
result_setlike or dislikeMust be omitted
resultflagRequired: the id of the record, as it was returned
  • request_id is the meta.request_id of the response you are voting on, or the run_id for an agent run's results.
  • entity_category is the category of the results (result_set) or of the flagged record (result).
  • reason_code is optional, and comes from the level's own vocabulary below. A like takes no reason and no text.
  • text is optional free text, up to 2,000 characters, and is accepted only together with a reason_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.

levelreason_code values
result_setnot_relevant, missing_expected, too_few, wrong_or_outdated, other
resultwrong_entity, outdated, duplicate, missing_info, other

One active vote, changeable

changedboolean

False when the same vote was already active — nothing was stored.

voteVoteView

The caller's own vote. text is theirs, so it is echoed back to them.

created_atstring
entity_categorystring
entity_idstring | null
level"result_set" | "result"
reason_codestring | null
request_idstring
textstring | null
vote"like" | "dislike" | "flag"
vote_idstring
  • You hold one active vote per subject: per request_id, level and, for a flag, entity_id. Sending a different vote replaces it.
  • Sending the same vote again (same vote, reason_code and text) changes nothing and returns changed: 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_idstring
votesarray of VoteView
created_atstring
entity_categorystring
entity_idstring | null
level"result_set" | "result"
reason_codestring | null
request_idstring
textstring | null
vote"like" | "dislike" | "flag"
vote_idstring

If 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:

  1. A report against a request joins 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.
  2. expected entries 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#

StatusCodeSummary
401auth.unauthorizedNo usable credential (cause deliberately unspecified)
403auth.browser_originA secret key was used from a browser
403auth.insufficient_scopeThe credential does not hold the required scope
404feedback.subject_not_foundThe search being reported is not one this credential ran
422pagination.cursor_invalidThe cursor could not be read (e.g. a filter changed mid-pagination)
422request.invalidA field failed validation (extra.errors has the detail)
503credits.unavailableThe billing service could not confirm the charge; retry with the same Idempotency-Key
503upstream.unavailableThe retrieval engine is unreachable (not charged)