# 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](/docs/api/reference/createCardFeedback) 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.

## Prerequisites

- A Salfio **API key** (`sk_live_…`). See [Authentication](/docs/api/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`](/docs/api/reference/listClients) 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_…`.

## 1. Read the client's cards

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.

## 2. Report the card

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.

## What the report does — and doesn't do

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](/docs/api/rate-limits).

## Errors you might hit

| 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](/docs/api/rate-limits). |

## Tips

**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](/docs/api/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.

## Related

- [`GET /v1/clients/{clientId}/cards`](/docs/api/reference/listClientCards) — read a client's cards.
- [`POST /v1/cards/{cardValueId}/feedback`](/docs/api/reference/createCardFeedback) — report an issue with a card.
- [Cards](/docs/cards) — what cards are, and how the dashboard's own **Report an issue** action works.
- [Authentication](/docs/api/authentication) — minting and presenting an API key.
- [Model Context Protocol (MCP)](/docs/mcp) — reading cards from an MCP client.
- [Conventions](/docs/api/conventions) — response envelope, error format, multi-tenancy.
- [Rate limits](/docs/api/rate-limits) — per-organization and per-endpoint limits.

## Related pages

- [Administrator Tools](./administrator-tools.md)
- [Agent Tools](./mcp-external-servers.md)
- [Assign a Slack channel to a client over the API](./guides-assign-slack-channel.md)
- [Authenticated health check](./api-reference-gethealth.md)
- [Authentication](./api-authentication.md)
- [Cards](./cards.md)
- [Changelog](./changelog.md)
- [Changelog](../changelog.md)
- [Connect a workspace](./getting-started-connect-workspace.md)
- [Connect an integration](./getting-started-connect-integration.md)

# Agent Instructions

This portal answers questions programmatically. To receive a synthesized,
source-cited answer instead of crawling page by page, append the `?ask=`
query parameter to any page URL on this site:

    /guides/quickstart?ask=how+do+I+authenticate

Optional parameters:

- `&goal=<what-you-are-trying-to-do>` steers the answer toward your
  objective (e.g. `&goal=write+a+python+client`).
- `&version=<label>` scopes the answer to a mounted version when the
  portal publishes more than one.

The response is `text/markdown`: the answer followed by a `# Sources` list
of the portal pages it was grounded in. Status codes are the contract:

- `200` — the answer; `402` — the portal owner’s plan or answer credits are
  exhausted (surface this to your operator; do NOT retry); `429` — you are
  rate-limited; back off for the `Retry-After` seconds; `503` — the answer
  lane is temporarily unavailable; fall back to crawling the `.md` pages.

For the full corpus map read `llms.txt` at the site root; for the tool
surface (search + page fetch as MCP tools) see `/mcp`.
