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.
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
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" }'
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
phase | Nominal period |
|---|---|
0 | day 0 → day 1 |
1 | day 1 → day 90 |
2 | day 90 → day 180 |
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.
- Collect every message with the same
thread.idfrom all webhooks. - Deduplicate by
wamid(message.id). - 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.
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 updateaction: "remove"→ remove
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
-
historyandsmb_app_state_syncsubscribed on the webhook endpoint - Endpoint answers
2xxin seconds and processes in the background - Deduplication by
wamidimplemented - Sorting by
timestamp, not by arrival order -
media_placeholderhandled, with later reconciliation bywamid - Contact timestamps read as milliseconds
- Triggered within the 24 hours