Skip to main content

Walkthrough: bulk feedback import

This walkthrough tours the Bulk feedback import console page end to end: what a feedback outcome is, the shape of one record, how the wizard validates a CSV, why re-importing is safe, and where the imported outcomes surface. For the conceptual overview, see the Bulk feedback import guide; this page is the hands-on tour.

1. When feedback matters

Orbit records a message’s delivery lifecycle — queued, sent, delivered, failed — from the carrier’s receipts. What delivery receipts can’t tell you is whether the recipient converted: opened the app, redeemed the offer, booked the appointment. That outcome usually lives in an external system — a mobile-measurement partner (MMP), your CRM, or an attribution vendor. Feedback closes that loop. Posting an outcome back onto each message stamps it confirmed (the intended business outcome happened) or unconfirmed (it explicitly did not). Once stamped, Insights surfaces and attribution reports can correlate conversion against delivery status, channel, campaign, and template — so macro-level quality reads (per-campaign conversion, per-template lift) work off real outcomes instead of guesses. Bulk import is how those outcomes arrive when the external system reports on a cadence — a nightly MMP conversion file, a weekly CRM export, a one-off vendor backfill.

2. The shape of one record

Each row is exactly two fields: confirmed covers positive conversion signals (delivered-and-read, link clicked, purchase, booking); unconfirmed covers messages you positively know did not convert. The enum is deliberately closed — the API rejects anything outside these two values, which keeps every downstream consumer of the feedback signal working off one vocabulary. Delivery lifecycle states (delivered, failed) come from carrier receipts, not from feedback. Optionally, the same endpoint accepts a Twilio-shaped envelope (Sid / Outcome field names) so an integration written against Twilio’s Message Feedback API ports unchanged.

3. The bulk import wizard

Open Messages → Bulk feedback import (/messages/feedback). The page is gated to owner, admin, and developer roles; members without one of those roles see the nav entry but cannot reach the form. Click Import feedback CSV, then paste or upload a two-column file:
Formatting rules, enforced before anything leaves the browser:
  • A header row is detected and skipped when its first cell is id, sid, message_id, or messagesid — so a Twilio-shaped Sid,Outcome export and a Devotel id,outcome export both import without manual stripping.
  • Outcome values are case-insensitive (Confirmed, CONFIRMED are accepted and lower-cased).
  • Ids must match msg_<32 hex>; legacy or wrong-shaped ids are listed as per-line errors with the line number, never guessed.
  • Duplicate ids are collapsed — first row wins — and the dialog reports how many duplicates it dropped.
  • A batch accepts up to 1,000 valid rows. Rows beyond the cap are dropped with a truncation warning; split larger exports into batches.
The wizard lists the valid row count, every per-line error with its reason, and the dropped-duplicate count. You fix the export before the request is sent.

4. Idempotency and retry semantics

Re-importing is safe. Each row lands through the same write path as the single-message endpoint (POST /api/v1/messages/:id/feedback):
  • Re-posting the same outcome for a message is a no-op — the stamped timestamp is not rewritten.
  • Flipping the outcome (e.g. unconfirmed → confirmed) refreshes the stamped timestamp, so the audit trail reflects the correction.
  • Feedback writes touch only your own tenant’s message rows. There is no billing effect and no delivery side-effect.
That means a nightly export job can safely re-run against overlapping windows — a message that was already stamped stays stamped, and only genuinely new or corrected rows change.

5. Reading the result

The API response returns 200 even when some ids match nothing, and breaks the batch into two lists:
  • updated[] — rows the write touched, with the stamped outcome and timestamp.
  • not_found[] — ids that matched no message in your account (retention purge, wrong tenant, or stale export). In the dashboard these render as an explicit list under Ids not found in this account. A not_found row is not worth retrying — reconcile the export upstream instead.
Once imported, the outcome lives on the message row and shows up wherever message-level signals are read:
  • Message detail — open a message in Messages and the stamped outcome appears alongside its delivery timeline.
  • Insights goals and attribution — the feedback signal joins campaign, template, and channel lineage, so per-campaign and per-template conversion reads can reconcile against the imported outcomes. For paid campaigns, the Campaign ROAS and revenue attribution guide shows how the credited-revenue side reads; imported outcomes give it a conversion-side signal.

6. The API path

The same import is available as a single endpoint, POST /api/v1/messages/feedback/bulk, accepting 1 to 1,000 rows per request:
A Twilio-shaped envelope ({"Items": [{"Sid": "…", "Outcome": "confirmed"}]}) is accepted as an alias, so a Twilio-integration bulk importer ports without a body change. A bulk import writes one audit-log entry for the batch with the requested / updated / not-found counts, so the import is traceable in your audit log as a single event rather than a thousand rows.

7. Role access

The console page runs behind a role gate: owner, admin, and developer can import; other roles are blocked at the route. The API endpoint itself is gated by your API key’s access, independent of dashboard role. If a teammate reports the page missing from the nav, check their assigned role first.