Pushing feedback
Single pushes, bulk backfills, file ingest, dedup, and back-dating.
Everything about getting data in, beyond the five-minute version in Getting started: which ingestion path to use, how deduplication and back-dating behave, and what happens after the push.
Three ways in
POST /feedback/push
A steady trickle into one field: each ticket, review, or call as it closes.
Synchronous; the response reports inserted and skipped_duplicates. Each item becomes its own respondent.
POST /feedback/push/bulk
Several fields in one call: a whole record per respondent, backfills, batch jobs.
Async 202 with a task_id and status_url by default; poll until completed. Small batches can run synchronously with "sync": true (below).
POST /feedback/upload
You already have a CSV or XLSX export.
Multipart upload; same async task flow as bulk.
All three land in the same place and trigger the same analysis. Prefer bulk over looping single pushes: one call for thousands of items is friendlier to your rate limits (60/minute per key by default) and to the platform.
Bulk push: one respondent, several fields
/feedback/push/bulk takes a list of fields, each with its own content array, and records the whole batch as one respondent's answers. That is how you push a full record (the open comment and its NPS score) as one row rather than as unrelated lines; the single-push endpoint, by contrast, treats every item as its own respondent. When each record is a different person, send one record per bulk call (the synchronous mode below is built for exactly that). An optional source_reference (up to 255 characters) is stored on every row of the batch, so you can tag which system or job produced it.
Synchronous bulk push
Integration platforms and webhook receivers usually deliver one record per call and cannot poll a task id. Add "sync": true and the batch runs inline, returning 200 with the completed per-field result instead of a task id:
curl -X POST https://boundaryai-ingest-279197672085.europe-west9.run.app/api/input/feedback/push/bulk \
-H "Authorization: Bearer $BAI_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-58121-v1" \
-d '{
"feedback_group_id": "1842",
"source_id": "9021",
"sync": true,
"source_reference": "crm-webhook",
"fields": [
{"field_id": "31245", "content": [{"text": "Support resolved my issue in one call.", "external_id": "ticket-58121"}]},
{"field_id": "31246", "content": [{"text": "9", "external_id": "ticket-58121"}]}
]
}'The rules that keep this safe:
Every row must carry an
external_id; otherwise the call is rejected withSYNC_REQUIRES_EXTERNAL_ID(400). An inline push can be cut short by its timeout, and the id is what lets a retry converge on the same rows instead of duplicating them.Small batches only: up to 50 items across up to 20 fields. Larger batches, and requests arriving while the inline capacity is busy, silently fall back to the asynchronous path and return 202 with a task id, so a client that always sends
syncstill works.Honest status codes: if no field accepted anything, you get 400
PUSH_FAILEDwith the per-field detail rather than a 200 that hides rejected rows. A push that exceeds its 60-second budget returns 504SYNC_PUSH_TIMEOUT; rows already written stay written, so retry with the sameexternal_idvalues.
Structured items: the fields that earn their keep
An item in content can be a bare string, but production integrations should send objects:
external_idis your stable identifier, and it's what makes retries safe: within a field, a second push with anexternal_idthat already exists is skipped, and the response counts it underskipped_duplicateswith the IDs induplicate_external_ids. Note that this is a skip, not an update: pushing a corrected text under the sameexternal_idleaves the original in place. Without anexternal_id, a replayed batch means duplicate comments polluting your analysis.occurred_atback-dates the item to when the feedback actually happened. This matters most for time-tracked groups: a June ticket pushed in July lands in June's period, not July's. Omitted, items date to arrival time.customer_idties items to a person across sources, powers bulk erasure, and pairs with theexternal_idon invites so you can join the full loop in your warehouse.channel,language,ratingbecome segmentation metadata, exactly like metadata columns on an upload.
Numeric fields (NPS, ratings) take their value as the item's text ("9"); non-finite values are rejected at ingestion.
Two layers of retry safety compose: external_id deduplicates at the item level forever, and the Idempotency-Key header makes an entire request replayable for 24 hours (same key = same response, no re-processing). Use both; networks fail mid-request more often than anyone likes.
What happens after a push
Items are cleaned, language-detected, and queued for analysis automatically; there is no "run analysis" call to make.
Group monitors with Auto-cover new sources on are already watching API-created sources (Custom Monitoring), so monitor matches and alerts fire from the first pass.
When the pass finishes, the
analysis.completedwebhook fires, andGET /sources/{id}/analysisreturns the sentiment distribution, themes, and monitor matches. Analysis takes minutes to tens of minutes depending on volume; until a run has completed, the read returnsanalysis_status: "none"with zeroed figures that are not results.For a source in a group that tracks feedback over time, the read aggregates every analysed period (the same corpus as the dashboard's Overall view) and returns
analysis_status: "available"withanalysis_id: null; key onanalysis_status, never onanalysis_id.The data participates in everything else: Grouped Themes, Evolution periods, reports, and the dashboards, indistinguishable from survey or upload data.
Pushing consumes your organisation's Usage Allowance when analysed, the same metering as every other analysis on the platform; see Settings. The push response's rows_accepted is the number of rows admitted (aps_deducted is a legacy alias of it); nothing is charged at push time, and test keys are never charged.
Erasing and listing what you pushed
You stay in control of the data your integration created:
GET /feedbacklists API-pushed items (filter bysource_id/field_id, cursor-paginated), useful for reconciliation jobs. It never includes native survey responses.POST /feedback/erasebulk-deletes byexternal_idsor bycustomer_id, which is the mechanical piece of honouring a GDPR erasure request: one call removes a person's API-pushed feedback across sources. It only touches API-pushed rows; survey responses and uploads have their own product-side deletion flows.
Integration checklist
One key per integration,
pushscope,Idempotency-Keyon every write.Always send
external_idandoccurred_at; your future self doing a re-sync will thank you.Use bulk for anything over a handful of items; poll the
status_urlor subscribe tocontent.pushed. Usesync: truewhen your caller needs the outcome in the same response.Subscribe to
analysis.completedinstead of polling the analysis endpoint.Map
customer_idto the same identifier your CRM uses, so erasure and invite joins stay one-call operations.
Last updated