Submitting Scraping Feedback
When a scraping run produces a user-visible result — correct or incorrect — surface a “was this right?” affordance and submit the answer through POST /v1/scraping-runs/:run_id/feedback. Feedback closes the loop between what you observe in production and the agent self-improvement pipeline.
The four signals
The endpoint accepts a small, deliberately narrow vocabulary. These are the four states a client (or its end-user) can actually observe:
| signal | Use when… |
|---|---|
| succeeded | Bill amount + due date + account match the user’s statement. Affirmative feedback is just as valuable as negative — it lets us auto-promote winning prompt versions. |
| login_failed | Login failed despite the user confirming valid credentials (often a biller DOM change). |
| bill_missing | Statement is visible on the biller site but the agent returned no bill. |
| data_incorrect | The bill came back but a field (amount, due date, account, etc.) doesn’t match the source statement. Put the specifics in notes. |
System-only signals (timeout, two_factor_required, extraction_empty) are tracked internally and are NOT in the client enum. Clients can’t meaningfully observe them.
1. Capture run_id + biller_id
Every scraping run returns a run_id and is associated with a biller_id. Persist both alongside the bill you display so feedback can be attributed back to the run that produced it and the biller it targeted.
When the user reports a problem on that bill, the report carries the run + biller reference.biller_id is required (it’s also what keys the per-biller rate limit).
2. POST the feedback
On user submission, POST to the endpoint with an Idempotency-Key so a network retry doesn’t create duplicate feedback. The key should be generated once at the moment the user clicks submit and re-sent on retry. Recommended format: idem_<timestamp>_<run_id>.
Minimal client-side submission
async function submitFeedback({ runId, billerId, signal, notes }) {
// Generated ONCE here. If the network errors and you retry, reuse this same key.
const idempotencyKey = `idem_${Date.now()}_${runId}`;
const submit = async () => fetch(
`https://sandbox.api.billerapi.com/v1/scraping-runs/${runId}/feedback`,
{
method: 'POST',
headers: {
'X-Client-ID': process.env.BILLERAPI_CLIENT_ID,
'X-Client-Secret': process.env.BILLERAPI_CLIENT_SECRET,
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
signal, // 'succeeded' | 'login_failed' | 'bill_missing' | 'data_incorrect'
biller_id: billerId, // required
notes, // optional, recommended for data_incorrect
}),
},
);
let res = await submit();
if (res.status === 429) {
// back off; per-(client × biller) limit is 100/hr
await new Promise((r) => setTimeout(r, 60_000));
res = await submit(); // safe: idempotencyKey is the same
}
if (!res.ok) {
const error = await res.json();
throw new Error(`feedback failed: ${error.error_code} — ${error.error_message}`);
}
return await res.json(); // { agent_type, run_id, ..., already_existed }
}3. Handle errors
The endpoint has 6 documented error codes. The full catalog is at Feedback errors. Short version for the common paths:
429 FEEDBACK_RATE_LIMITED— back off. The limit is per(client_id, biller_id); if many users are submitting on the same biller, queue.409 FEEDBACK_ALREADY_SUBMITTED— feedback already exists for this(client_id, run_id). Each run accepts exactly one submission. Treat as a successful outcome if you didn’t mean to overwrite.404 FEEDBACK_RUN_NOT_FOUND— run_id doesn’t exist. Don’t expose to the user; log + investigate.403 FEEDBACK_RUN_NOT_OWNED— the run_id exists but belongs to a different client. Audit your run_id storage; you should never see this in normal operation.410 FEEDBACK_RUN_EXPIRED— the run is older than 30 days. Feedback windows are bounded so the brain doesn’t train on stale signal.422 FEEDBACK_INVALID_CATEGORY— signal is missing or not one of the four enum values. Fix client-side validation; this shouldn’t reach users.
What happens to your feedback
Submitted feedback enters the per-biller signal pipeline:
- succeeded — counts toward auto-promote thresholds for the prompt version that ran. Affirmative feedback is what lets winning versions graduate.
- login_failed / bill_missing / data_incorrect — counts toward auto-rollback thresholds. A cluster (≥5 in 24h on the same biller) crosses the threshold and reverts the agent to its prior prompt version without human intervention.
This is why high-quality feedback matters. A noisy stream of unhelpful reports (mis-categorized signals, empty notes) dilutes the signal. Gate on user confirmation, populate notes with the specifics, and the per-biller pipeline compounds.
Best practices
- Submit on success too. Most clients only file negative feedback. That biases the signal.
succeededsubmissions are what let us auto-promote new prompt versions safely. - Don’t auto-submit on every model output you don’t recognize. Gate on user confirmation. False-positive feedback poisons the signal.
- Don’t reuse
Idempotency-Keyacross different submissions. The key is per-submission, not per-user-session. - Use
notesfor specifics ondata_incorrect. “Amount is wrong” is noise; “$0 returned but statement shows $84.32” is actionable. - Check
already_existedin the response. Lets you distinguish “new feedback recorded” from “idempotent replay” without comparing timestamps.