Skip to main content

API recipes: message-history import and reconciliation

The per-provider guides land your configuration (numbers, services, contacts, templates) on Orbit; message history needs its own loop. This page is that loop: ingest historical rows as messages, keep the source-to-Orbit id mapping, and reconcile until both sides agree. Base URL is https://api.orbit.devotel.io/api/v1 throughout; authenticate with X-API-Key.
There is no bulk “post my history” endpoint. Orbit ingests configuration through the assisted import wizard (whose conversations entity pulls up to 90 days of Twilio history server-side), and everything else lands through the same message API you send with today. Your migration script owns the mapping between source ids and Orbit ids; the recipes below are the reads and writes that make that mapping provable. These are tenant-owned controls: Orbit supplies and enforces the surface, you own the source credentials, the mapping store, and the go-live call.

1. Ingest: post historical rows as messages

POST each historical row to the unified send endpoint, one message per source row. Keep the body to what the row actually carried: the phone pair, the channel, and the body. Send a stable Idempotency-Key per row (derive it from the source message id) so a retried post after a 429 or a timeout can never double-import the row.
The response carries Orbit’s own id, a msg_… string. Store the pair (source id → msg id) in your mapping table the moment the post succeeds; Orbit does not echo or persist your source id on the message row, so the mapping table is the only place the two sides join. The channel list for the unified send (sms, whatsapp, and the rest) is the one documented at API recipes: third-party migrators and the per-channel guides; WhatsApp history beyond Meta’s own window arrives as free-text records, not templates. For WhatsApp cutover specifics, including the 24-hour customer-care window on new sends, see Migration from Twilio. Rate-limit your loop to the key’s allowance: on 429 RATE_LIMITED, read Retry-After (and error.retry_after), wait, and retry with the same Idempotency-Key.

2. Map: keep source ids where you can prove them

Two levels of mapping, matching what each surface actually stores:
  • Messaging services carry the source id in Orbit itself: create the service with metadata.external_id set to the old Messaging Service SID (SM…), exactly as loop 1 of the migrator recipes shows. History rows you post under that service then inherit a resolvable source handle.
  • Messages carry Orbit’s msg_… id only. Your mapping table is the join: source_id → msg_id, written in section 1, read back in sections 3 and 4. Do not encode source ids in the message body to smuggle them through; that pollutes the transcript your agents and inbox render.

3. Read-path reconciliation: agree on what landed

Reconcile in two passes. First, spot-check the mapping: GET /messages/:id for a sample of mapped ids (and every row your ingest flagged as retried).
cURL
Node.js
Branch on the outcome: Orbit’s message read path returns 404 for absent rows; it has no tombstone state on reads. If your source system answers 410 Gone for deleted history, treat that as “removed by design” and count the row as reconciled-removed rather than missing: Orbit intentionally does not replicate source-side deletions it never observed. Second, count-check the whole window: POST /messages/export pulls up to 10,000 rows with the same filters the list accepts (channel, status, from_date/to_date), so per-day buckets give you an exact landed-count per channel to compare against the source’s per-day count.
cURL
Convert the export to CSV and diff column-by-column against the source export; the column semantics are documented at Message export columns vs CSV.

4. Delivery receipts: reconcile statuses, not just rows

A history row is only as good as its final status. Subscribe one tenant-wide webhook endpoint to message.delivered and message.failed (the loop at third-party migrators shows the envelope), and match events to source rows through your mapping table on data.message_id. For historical rows that predate the cutover, the source’s terminal status is authoritative: copy it into your reconciliation report rather than waiting for Orbit receipts, since a row imported after delivery will not generate a new carrier receipt. Where a source system spooled undelivered mail or SMS for later retry (the IMAP-spool and imap.raise failure class some legacy SMS gateways document), do not try to replay the spool into Orbit: post the row once with its terminal status recorded in your report, and let Orbit’s own delivery pipeline own everything from the cutover moment forward. Scope the receipt comparison to HTTP status plus message.delivered / message.failed events; anything the source retried internally shows up on your side as a single final status. Outbound traffic never routes through your old provider as part of an import: history ingest writes rows into Orbit, and live sends exit through Orbit only.

Errors as recipes

See also