Skip to main content
Salfio Docs

Search documentation

Type to search this documentation.

On this pageOverview

Forward card feedback from your own product

Read a client's cards over the API, then report the ones your users reject — so card quality is measured against real signal instead of anecdotes.

If you render Salfio cards inside your own product, your users are the first people to notice when a card is wrong. The card feedback endpoint is where those reactions reach us: forward the reason a user rejected or corrected a card, and it lands in the same place the dashboard's Report an issue dialog writes to.

This guide walks the loop end to end:

  1. Read a client's cards to get the card_value_id of the card your user is looking at.
  2. Report that card with a category and, optionally, what your user actually said.

Two calls. The thing that trips people up is picking the wrong id — see the note between the steps.

  • A Salfio API key (sk_live_…). See Authentication for how to mint one and how to present it. Either key type works, and the choice decides attribution: a User key records its creator as the reporter, while a Service key carries no user, so the report is stored with no reporter attached. Both are accepted.
  • The client id whose cards you're rendering. List your clients with GET /v1/clients if you don't have it yet.
  • Nothing else. A client's card values are created with the client, so every card already has a card_value_id before it has generated anything. A card still working has value: null, and reporting one in that state is legitimate feedback.

All examples use the base URL https://api.salfio.com/v1 and pass the key as Authorization: Bearer sk_live_….

List the cards you're rendering. The response is ordered by display position and filtered to the cards that are active and displayed.

curl -s https://api.salfio.com/v1/clients/ad44…/cards \
  -H "Authorization: Bearer sk_live_…"

{
  "data": [
    {
      "card_value_id": "3f9c1e8a-77b2-4c15-9d40-2a6f0b8e1c33",
      "card_config_id": "b21e5d70-9f3c-4a88-b6e1-0c7d42fa9915",
      "identifier": "briefing",
      "title": "Briefing",
      "display_order": 1,
      "value": "Acme renewed the Growth tier in March…",
      "last_update": "2026-08-20T09:14:00Z",
      "is_stale": false,
      "render_mode": "insights"
    }
  ],
  "meta": {}
}

Keep card_value_id. That is this client's own instance of the card — the thing that actually holds the value your user read, and the id the feedback endpoint expects.

Two other fields matter if you are rendering the card yourself. render_mode tells you how to treat value: insights means it carries the structured {items: […]} envelope, freeform means it is plain Markdown to render as-is. is_stale means the underlying conversations have changed since value was written, so what you are showing is behind. A card generated with the help of a connected agent tool also carries a grounded_on object describing what that generation read; it is absent from activity-only generations, which is most of them.

Reading is not free of side effects. When any of the client's cards are stale, this call moves them up the regeneration queue. That is deliberate — it is how a read keeps cards fresh — but it means a refresh can land between your user seeing a card and you reporting it.

Do not send card_config_id. It looks interchangeable and is not: the config is the card definition, shared by every client in your organization. Posting it to the feedback endpoint returns 404 not_found, because no card value has that id.

MCP: call the list_cards tool with { "client_id": "ad44…" }. It returns the same cards, minus the render metadata — render_mode comes back empty and grounded_on is not included. Use the REST endpoint if you need either.

The two calls answer in different field casing

This read endpoint is snake_case (card_value_id, display_order, last_update). The feedback endpoint in step 2 is camelCase, like the rest of the API — its response returns createdAt, not created_at.

The request body in step 2 has nothing that can be mis-cased, so this only bites when you deserialize the two responses into typed structs: they need different naming conventions. The id you pass is the thing to get right.

POST the report against the card_value_id you kept. category is required; details is optional free text.

curl -s -X POST https://api.salfio.com/v1/cards/3f9c1e8a-77b2-4c15-9d40-2a6f0b8e1c33/feedback \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "wrong_facts",
    "details": "The renewal date is off by a month — the contract runs to March, not February."
  }'

The four categories are fixed. Pick the one that describes the kind of problem, not how serious it is:

Category Use it when
wrong_facts Names, numbers or dates that don't match reality.
speculative_deduction The facts are right, the conclusion isn't.
wrong_language_or_format Wrong language, currency, dates or layout.
other Anything else — pair it with details.

The category values stay snake_case even though the field names are camelCase; send them exactly as written above.

details is capped at 5000 characters, counted in Unicode characters rather than bytes. Anything longer is rejected with 400 invalid_argument rather than silently truncated. Passing your user's own words through is more useful than a summary — that free text is the part a human reads when working on card quality.

A stored report answers 201:

{
  "data": {
    "id": "9d41b7c2-58ea-4f30-a1d6-3b90e7c25f48",
    "createdAt": "2026-08-21T10:32:11Z"
  },
  "meta": {}
}

Keep the id if you want to quote the report when following up with us.

No MCP equivalent. Unlike step 1, filing feedback is REST-only — there is no create_card_feedback tool. An MCP client can read cards but must call this endpoint over HTTP to report one.

Be careful how you present this in your own UI, because the report is deliberately inert:

  • It never hides the card and never triggers a regeneration. The card your user just rejected will still be there on the next render, unchanged.
  • It is never shown to other users in the organization.
  • It notifies no one. There is no alert, no digest, no aggregation.
  • There is no endpoint to read reports back. The write is one-way; the id in the response is the only handle you get.

Reports are recorded for the Salfio team to analyse when working on card quality. If a card is consistently wrong because its prompt is wrong, the fix is editing that prompt under Settings → Cards — that is what actually changes the output.

Alongside your category and details, Salfio captures the card's content, identifier and title as they were when you reported them. Card values are overwritten in place whenever they refresh, so without that snapshot the reported content would be gone before anyone could act on it.

Duplicates are accepted on purpose. Reporting the same card repeatedly is meaningful — repeat reports are the strongest quality signal available — so identical submissions are all stored rather than deduplicated. Don't build client-side de-duplication to be polite; just stay inside the rate limits.

Status Code When
400 invalid_argument category missing or not one of the four values; details over 5000 characters; the path parameter isn't a valid UUID.
401 unauthorized Missing, malformed, revoked, or expired key.
404 not_found No card value has that id or it belongs to a different organization — including when you sent a card_config_id by mistake. The API returns 404 either way; it never reveals cross-tenant existence.
429 rate_limited You hit the org or endpoint rate limit. Honour Retry-After. See Rate limits.

Report at the moment of rejection. The snapshot is taken when the report lands, not when your user saw the card. Cards refresh in the background, so a report queued for hours may snapshot a value your user never read.

Batching across many clients. The two calls compose cleanly in a loop — read cards per client, report as needed. Mind the rate limits: 100 requests per minute per organization, 50 per minute per endpoint.

Map your own taxonomy onto the four categories. If your product already collects refusal reasons with its own labels, map them to the closest category and put your original label in details. A consistent mapping is worth more than an exact one.

Export
Suggest an edit to this page

Documentation menu