Bulk feedback import
Bulk feedback import posts conversion outcomes back onto the messages you already sent: for each message id, whether it produced the intended business outcome (confirmed) or explicitly did not (unconfirmed). It is the bulk ingest path for attribution data that lives outside Orbit — a conversion report from a mobile-measurement partner (MMP), a CRM export, or an attribution vendor — applied to thousands of message rows in one request.
This fills the gap post-call surveys and Orbit’s own attribution don’t cover: surveys capture the voice of the customer who answered, and Orbit’s funnel attribution only sees conversions that happen inside Orbit. When an external system owns the conversion signal — an app install the MMP attributed, an order your CRM recorded, an appointment your vendor booked — bulk feedback is how those outcomes get stamped onto the outbound messages that earned them.
Per message, posting an outcome is a Twilio Message Feedback parity call. The bulk variant is the same per-row write batched: one envelope, one database update — roughly 1000× faster than per-message POSTs at 10k+ row import sizes — and it accepts a Twilio-shaped export as-is.
The dashboard surface: one CSV of outcomes, uploaded once
Open Bulk feedback import under Messages (owner, admin, or developer role) and click Import feedback CSV. Paste or upload a two-column CSV — the canonical message id, then the outcome:- A header row (
id,outcome— or a Twilio-shapedSid,Outcomeexport) is detected and skipped; no manual stripping needed. Twilio’s Pascal-casedOutcomevalues are accepted in either casing. - Ids must be canonical
msg_<32 hex characters>. Retention-purged or wrong-tenant ids are reported back, not guessed. - Duplicate ids are collapsed — first row wins.
- A batch caps at 1000 valid rows. Row 1001 and beyond are dropped with a truncation warning; split larger exports into batches.
The API path
POST /api/v1/messages/feedback/bulk accepts 1 to 1000 {id, outcome} rows per request:
{"Items": [{"Sid": "…", "Outcome": "confirmed"}, …]}) is accepted as an alias, so an integration written against Twilio’s Message Feedback bulk update ports without a body-shape change. Full request/response schemas: Messaging API reference.
Every valid row lands in a single database update. Each row behaves exactly like the single-message endpoint (POST /api/v1/messages/:id/feedback):
- Re-posting the same outcome for a message is a no-op; flipping the outcome refreshes the stamped timestamp for the audit trail.
- Rows write only to the tenant’s own messages — feedback is a caller-side signal with no billing and no delivery side-effect.
- A bulk import writes one audit-log entry for the batch as a whole, carrying the requested / updated / not-found counts.
Attribution loops
Imported outcomes are the same feedback signal the single-message endpoint writes — they join the message rows Orbit already stores, so every report that reads message-level signals can correlate outcomes with delivery status, channel, and campaign lineage. The loop closes when you pair the import with an attribution read. For click-to-WhatsApp ads,GET /api/v1/ads/ctwa/attribution joins captured click-to-chat entry points, orders, and ad spend into one report — the view an MMP export of WhatsApp campaign attribution feeds into. For paid campaigns generally, the Campaign ROAS and revenue attribution guide shows how credited revenue reads per campaign, template, and message; outcomes you import give the downstream reports a conversion-side signal to reconcile against. Cross-check with the Meta Ads attribution guide when the spend side lives in Meta.
Error handling
The response returns 200 even when some ids match nothing — the batch as a whole succeeded, and per-row outcomes are yours to process:updated[]— rows the update actually touched, with the stamped outcome and timestamp.not_found[]— ids that matched no message in your account. Typical causes: the message was purged by your retention window, the export came from a different tenant or a previous export with stale ids, or the id never existed here. In the dashboard these render as an explicit id list under Ids not found in this account.- Client-side, before the request ever leaves the browser, the import dialog reports per-row rejections with their reason — malformed id shape, an outcome value outside
confirmed | unconfirmed, or a line that isn’t two columns — each tagged with its line number in your file.
not_found row is not a failure to retry — the row no longer exists to tag. Reconcile the export upstream (re-run the report fresh, or accept the retention drop) rather than re-posting the same ids.
When to use which ingest path
Use bulk feedback import when the attribution lives outside Orbit and arrives on a reporting cadence — a nightly MMP conversion file, a weekly CRM export, a one-off vendor backfill. One upload reconciles a whole report; per-message POSTs at that volume burn thousands of round trips for the same result. Use real-time webhooks plus the single-message endpoint when the conversion signal is yours and arrives live — your own checkout handler, your appointment system, an in-app event. PostPOST /api/v1/messages/:id/feedback from the webhook consumer the moment the outcome is known, and keep bulk import for the periodic reconciliation pass.
Use post-call surveys or the in-product attribution funnel when the signal already lives inside Orbit — nothing to import.