Skip to main content

Sync history and contacts

When you connect a number in coexistence, you can bring the last 6 months of conversation history and the device's contact list into the Cloud API.

Read this before triggering

You have 24 hours from Embedded Signup, and one attempt per sync type. Miss the deadline or get it wrong and the customer has to redo the entire onboarding. There is no recovery.

Before triggering​

1. The customer approved sharing on their phone. During Embedded Signup the app asks permission to share history and contacts. If they declined, nothing will be sent — and the trigger doesn't error, no webhook simply ever arrives.

2. The events are subscribed. Without this, the data is sent and discarded.

curl -X PATCH https://api.joinotify.com/webhook-endpoints/whe_01J8ZK... \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "events": ["messages", "history", "smb_app_state_sync"] }'

3. Your server can take the volume. A single webhook can carry thousands of messages. Answer 2xx immediately and process in the background.

Triggering​

Conversation history
curl -X POST https://api.joinotify.com/v1/106540352242922/smb_app_data \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "messaging_product": "whatsapp", "sync_type": "history" }'
Contact list
curl -X POST https://api.joinotify.com/v1/106540352242922/smb_app_data \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "messaging_product": "whatsapp", "sync_type": "smb_app_state_sync" }'

The response only confirms scheduling. The data arrives later, asynchronously.

How history arrives​

Through multiple webhooks, split into phases and chunks.

webhook (1 POST)
└── value.history[]
└── threads[] ← 1 thread = 1 conversation with 1 contact
└── messages[] ← messages in that conversation

Phases​

phaseNominal period
0day 0 → day 1
1day 1 → day 90
2day 90 → day 180
The phases overlap

In practice, messages from any date show up in any phase. Don't use the phase to infer a period. Sort everything by timestamp.

Chunks and progress​

Each phase can arrive in several chunks (chunk_order), not necessarily in order.

{ "phase": 1, "chunk_order": 3, "progress": 60 }

progress runs from 0 to 100. progress: 100 on the last phase means the history is complete — that's your signal to wrap up processing.

Deduplication — required​

The same thread.id shows up in several webhooks, and the same message can arrive twice.

  1. Collect every message with the same thread.id from all webhooks.
  2. Deduplicate by wamid (message.id).
  3. Sort by timestamp — Unix in seconds, 10 digits.
const conversations = new Map();

function processHistory(value) {
for (const block of value.history ?? []) {
for (const thread of block.threads ?? []) {
const current = conversations.get(thread.id) ?? new Map();
for (const msg of thread.messages ?? []) {
current.set(msg.id, msg); // dedup by wamid
}
conversations.set(thread.id, current);
}
}
}

function finish(threadId) {
return [...conversations.get(threadId).values()].sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
}

Message direction​

history_context.from_me: true means the business sent it, false or absent means it was received.

Media inside history​

Media does not travel inside the threads. It shows up as type: "media_placeholder", with no content:

{ "id": "wamid.HBgN...", "type": "media_placeholder", "timestamp": "1785312000" }

The real file arrives in separate webhooks (value.messages or value.message_echoes), matched by wamid.

Only ~14 days of media

Files older than that have no content available — the placeholder is all you get. That's a Meta limitation, not a Joinotify one.

How contacts arrive​

Usually in a single webhook, in the state_sync array. Large accounts can produce several, with no guaranteed ordering.

{
"field": "smb_app_state_sync",
"value": {
"state_sync": [
{
"type": "contact",
"action": "add",
"contact": { "phone_number": "5541999999999", "full_name": "Maria Silva" },
"metadata": { "timestamp": "1785312000000" }
}
]
}
}
  • action: "add" → add or update
  • action: "remove" → remove
Different time unit

Contacts use milliseconds (13 digits); history uses seconds (10 digits). Mixing the two produces dates in 1970 or in the year 57000.

After the initial sync​

Later contact changes keep arriving automatically through smb_app_state_sync — no need to call the endpoint again. The one-shot rule applies only to the initial load.

Checklist​

  • Customer approved sharing on their phone
  • history and smb_app_state_sync subscribed on the webhook endpoint
  • Endpoint answers 2xx in seconds and processes in the background
  • Deduplication by wamid implemented
  • Sorting by timestamp, not by arrival order
  • media_placeholder handled, with later reconciliation by wamid
  • Contact timestamps read as milliseconds
  • Triggered within the 24 hours