Your outbound stream already answers "what did we send" and "did the carrier deliver it." The question that closes the attribution loop — "did the recipient actually convert" — usually lives somewhere else: a mobile-measurement partner (MMP), a CRM, or an attribution vendor's nightly export. Devotel Orbit ingests that outcome-side data on Messages → Tools → Bulk feedback import, which posts POST /api/v1/messages/feedback/bulk, so the conversion truth you already hold elsewhere lands back on the message rows your goals and route-preview surfaces report on. This post walks the parity contract behind it, why outcomes arrive in bulk rather than as a per-event stream, how the import reconciles against your sends, the import patterns that hold up, and the tenant-owned privacy choices involved.
What "conversion feedback bulk parity" means
Twilio's Message Feedback API is the original shape: a caller reports the outcome of a message — confirmed (the recipient converted) or unconfirmed (sent but no conversion). Twilio's own surface is one message per call, one row at a time. Orbit honors the same contract and adds a bulk variant: one envelope carrying 1 to 1,000 {id, outcome} rows, persisted in a single statement on the server — roughly 1,000× fewer round trips at the 5k–50k import sizes MMP exports produce. The Bulk feedback import console in the dashboard wraps the endpoint with a CSV uploader; POST /messages/:id/feedback remains the single-message sibling for live integrations. Same outcome enum, same idempotent write semantics, one ingestion path.
Why outcomes arrive in bulk, not as a per-event stream
Delivery-side events — sent, delivered, failed, replied — stream in as provider callbacks or recipient interactions. They arrive per-event because each one originates on a live path and is useful the moment it lands. Conversion outcomes are different: the conversion happens on your web, app, or CRM surface, and the tooling that records it — the MMP SDK, the CRM write, the attribution vendor's model — almost always exports on a schedule, not inline. A nightly CSV of 5k–50k conversions is the normal shape of this data.
That pushes the ingestion decision: per-message POSTs at 50k rows burn 50k round trips plus auth and rate-limit overhead; a bulk envelope carrying the same rows turns it into a handful of calls and one statement each. The dashboard uploader accepts a two-column CSV (id,outcome — Twilio-shaped Sid,Outcome headers work too, as do the aliases message_id and messagesid), de-duplicates repeated message ids client-side, and caps each batch at the endpoint's 1,000-row ceiling with a truncation warning when the export overshoots — scripts paginate at the same boundary.
Recipient-side attribution: how the import finds the message and campaign mapping
The import reconciles against your tenant's own message rows. Each row carries a message id, and the write lands only on rows that exist; the response splits ids into updated and not_found so a mismatched export surfaces exactly which ids failed to reconcile, with summary counts either way (still HTTP 200 — the batch succeeded; per-row misses are the caller's to chase).
Because the outcome is stored on the message row, it joins the campaign and channel mapping every send already carries — the same join keys the goals & attribution funnel and the route preview compare-routes-before-you-send surface read. That is the recipient-side attribution fit: the outcome-side event lands on the same grain as the touchpoints, so multi-touch credit models, funnels, and route comparisons see the conversion without a new storage layer. Route preview shows you the channel the smart router would pick before you send; feedback import then tells goals whether the recipient converted afterward — the post-send half of the same evaluation loop.
OK import patterns — mapping an MMP / CRM export
Patterns that hold up in practice:
- Two columns at minimum.
id(or Twilio'sSid) plusoutcome. Extra columns the export carries —type, value, occurred_at, conversion value, timestamps — are metadata for your downstream reporting; the ingestion contract only needs the id and the outcome ('was it converted'), and the importer ignores the rest. - `confirmed`, `unconfirmed`, or skip. The enum is closed and binary. Rows whose attribution case is ambiguous ('converted, but we can't be sure this message drove it') should stay out of the batch rather than ship as a third value.
- Collapse duplicates before you POST. Update tools like AppsFlyer and Adjust often emit multiple touch rows for one conversion; the endpoint rejects a batch containing the same id twice, so dedupe client-side (first or last wins — your semantics, decided before the call).
- Chunk to the 1,000-row cap, or let the dashboard importer do it for you and warn when truncation cut the tail.
- Map the id at export time. The canonical id shape is
msg_followed by 32 hex characters; exports keyed to a legacy id format get rejected at validation, so normalize before upload, not after. - Choose your header deliberately.
id,outcomeis canonical;Sid,Outcomeis accepted for a Twilio direct port. Pick one and keep it consistent — mixed-header files across the team are the most common avoidable import breakage.
Connectors — sending-side vs importing-side
Two integration directions cover this loop, and they do different jobs:
- Sending-side connectors (Meta CAPI, an MMP SDK, a CRM webhook). Your platform emits conversions out to the measurement stack. That is the right pattern when the external system owns attribution and reporting from live events as they happen.
- Importing-side (Orbit bulk feedback import). The outcome data already exists elsewhere, and you pull it back into Orbit so goals, attribution, and route preview close the loop on Orbit's own message rows. That is this page's job, and the two patterns are complementary rather than competing.
Most teams run both: stream out to the MMP for live measurement, and import the settled outcome back in bulk for the feedback loop against the sends that drove it.
Tenant-owned privacy posture for outcome-side data
'Outcome-side' data — the fact that a recipient converted — does not inherit the consent envelope of the original send by default. Whether it must, and which lawful basis applies, is a tenant decision, not a platform mandate. Orbit treats the import as a tenant-owned control (it is not compliance-gated by the platform), so register your reasoning explicitly — which of the GDPR/CCPA/TCPA regimes you operate under, which lawful basis you rely on for outcome storage, and what retention window you apply. Honest framing: conversion flags on message rows are personal data, and some regimes (GDPR legitimate-interest vs consent, LGPD, others) change what you may record per recipient. Your legal counsel makes that call; Orbit's stance is to surface the decision, not make it for you.
FAQ
Twilio parity deviations — what differs from the wire shape I know?
The envelope (items vs Twilio's Items), the row keys (id/outcome vs Sid/Outcome) — both spellings accepted — the 1,000-row cap (Twilio has no published bulk shape), and legacy-id rejection (Twilio's SM… SIDs map to Orbit's msg_<32-hex>).
Per-call vs bulk semantics — do they behave differently?
Only in throughput. The same outcome enum, the same idempotency rule, the same closed list of acceptable values; bulk runs one statement per request instead of one per row. Per-call stays right for a live integration that resolves outcomes one at a time; bulk is for batch exports.
Queue back-offs — what if a batch partially misses or the file is oversized?
Chunk at the 1,000-row cap. not_found ids return in the response body for retry against the correct export (no batch-level failure). Write-load stays bounded by the cap; rate limits apply to the calls you make, so pacing chunks every few seconds is well inside budget.
Audit — where is the import recorded?
Every import is audited the way other message mutations are — the operator, the row count, the updated/not-found split, and the timestamp land in the audit log, and the stored outcome carries the import source (bulk vs single-message) so a report can tell an imported outcome from a live POST.
Does the import change billing or delivery status?
No. Feedback records an outcome flag on the message row. Delivery status, DLR handling, and billing are untouched — the import is analytics-side attribution data, nothing else.
Close the loop in Outbound
- Goals & attribution — funnels and multi-touch credit models that consume the outcomes you import.
- Route preview — compare the smart-router's channel choice and cost before you send.
- Bulk feedback import console — upload the CSV or POST the envelope; updated vs not-found comes back per batch.
- Messages overview — per-recipient drill-down when a row needs eyes on it.
Import the outcome-side truth you already have, and outbound goals stop extrapolating from receipts alone.