Customers see an honest pending or reconnecting state and accepted messages are not duplicated or silently discarded.
Use stable idempotency keys
Generate one idempotency key for each user intent and retain it until the SDK reports an accepted result. The native engine stores the exact request body, timestamp and key in its account-scoped durable outbox before resolving the write. Do not generate a new key just because the acknowledgement is delayed.
The web widget owns its customer-facing reconnecting and pending UI. Mobile headless clients expose resumeSession, refresh and flushOutbox so the host can provide an explicit retry control after the same account receives a fresh token. The code below uses the TypeScript wrapper shape (startSession(message, options) and sendMessage(text, idempotencyKey)); Dart uses the same operations with sendMessage(text, idempotencyKey: key). Direct Swift and Kotlin clients use their platform input DTOs with the same stable-key rule, as shown in their platform references.
const sessionKey = `order-${orderId}-session-${attemptId}`;
const messageKey = `order-${orderId}-question-${attemptId}`;
// Persist each key with its own user intent until the SDK acknowledges it.
await client.startSession("I need help with my order", {
idempotencyKey: sessionKey,
});
await client.sendMessage("The order number is ORD-42", messageKey);
// Call this only from an explicit "Retry the same message" control after
// connectivity and a fresh token for the same account are available.
async function retryPendingMessage() {
await client.resumeSession();
await client.flushOutbox();
}Distinguish pending, rejected and duplicate outcomes
A network or throttling error can leave admission uncertain, so retain the original mutation for an explicit same-key retry. A field-invalid, payload-too-large, message-rejected or idempotency-conflict error needs host correction or investigation and must not be replayed with a different key automatically.
A session-expired result requires a fresh signed token for the same account before retry. Do not call resetSession for a transient outage because reset intentionally clears the durable session and pending mutations.
Make offline state visible
- Show a pending or reconnecting label tied to the SDK loading state and keep the composer available only when the host can safely accept another intent.
- Offer Retry the same message after connectivity and authentication recover. Keep the original idempotency key behind the control and disable duplicate submit while it is in flight.
- If the user intentionally discards the conversation, call resetSession and make the discard result explicit. Never clear durable state just because the app was backgrounded.