How WarmySender verifies InMail delivery (and what to do if your daily cap shows the wrong number)
If your dashboard says an InMail was sent but you can't find it in your LinkedIn Sent folder — or if your campaign shows 'Daily limit reached' when you know you haven't actually sent that many — this guide explains how WarmySender verifies InMail delivery, why InMails sometimes show as queued but don't deliver, and what's happening when your cap counter shows the wrong number.
Why InMails sometimes show as queued but don't deliver
A LinkedIn InMail can fail to deliver for reasons that look identical to success at first glance:
- Recipient eligibility — InMail can only be sent to recipients who allow InMails (most Premium / Sales Navigator / Recruiter members do; Free members can opt in via Open Profile). If a recipient has tightened their settings, the send is rejected.
- Sales Navigator credits — Each InMail consumes one Sales Nav credit. If your account is out of credits, the send is rejected even though every other check passes.
- Open Profile fallback — If you're sending via the 'classic' InMail API and the recipient has Open Profile enabled, the send goes through without consuming a credit. If their Open Profile is off, the send needs a credit and falls back to standard InMail rules.
- Rate limits and ramp — LinkedIn's per-account InMail limits change with your account type and how long you've been sending. Sales Nav veteran accounts can typically send up to 50 InMails per day; new accounts ramp gradually from much lower numbers.
- Connectivity gaps — A recipient on an account in transit, or a temporary LinkedIn-side issue, can return a soft failure that looks like 'sent' from the API's vantage point.
In every case above, LinkedIn's InMail API can return a 200-OK shape that looks like success on the wire — without actually creating the conversation on the recipient's side. WarmySender's job is to tell those apart.
How we verify delivery via chatId
When WarmySender sends an InMail through Unipile (our LinkedIn integration partner), the API returns two fields: id (the message id of the first message in the new chat) and chat_id (the chat container id). The chat container is the proof a real conversation now exists on LinkedIn — a chat_id means LinkedIn accepted the InMail and a recipient-side thread was created. No chat_id, no conversation.
The delivery gate works like this:
1. WarmySender sends the InMail to Unipile.
2. Unipile returns a response. We extract the chat_id (chat-id-first; we fall back to id only if chat_id is absent).
3. If the response carries a real chat_id, we treat the send as successful: the campaign event row is written, the dashboard counter advances, your prospect's last-messaged-at is stamped, and the conversation is linked into your unified inbox.
4. If the response has no chat_id (the silent-drop signature), we treat the send as failed: NO event row is written, NO counter advances, and the prospect is NOT marked as messaged. The send is classified as a phantom and your daily cap is preserved.
This is the chatId proof-of-delivery contract. It guarantees that every number on your dashboard reflects a real conversation that exists on LinkedIn, not a wire-level success that quietly bounced.
A practical example: 18 days of phantom InMails caught by the chatId requirement
On May 2, 2026 we found a regression where a missing field-name in our extractor caused every InMail to look like a silent drop — 1,104 InMail events platform-wide over 18 days had no chatId attached. The new chatId gate (deployed earlier that morning) correctly refused to count any of them. We deployed a follow-up fix the same day to make the extractor read the right field (chat_id, not id), so real chatIds now flow through and real InMails count.
The key takeaway: the gate did its job. Even when the upstream extractor was broken, the platform refused to inflate your dashboard numbers with phantom sends. That's the design — your dashboard tells you what reached LinkedIn, not what we hoped reached LinkedIn.
What happens if your daily cap shows the wrong number
The daily cap on each LinkedIn campaign counts how many sends have happened today. If the counter is poisoned by historical phantom rows (events written before the chatId gate landed), it can read 40/40 even though zero real InMails have actually been sent today.
We shipped a companion fix on May 2, 2026 to handle this exact case:
- Every historical phantom InMail event (1,104 rows platform-wide, going back to mid-April) is annotated with a metadata flag — phantom_send_2026_05_02 = true. The rows aren't deleted; they're preserved for audit, refund eligibility checks, and customer-comms accuracy.
- The daily-cap calculation in canSendToCampaign now subtracts annotated phantom rows from the per-campaign cap. Pre-fix the math was: today's count = total InMail events for this campaign today. Post-fix: today's count = max(0, total − phantom_excluded). On a clean campaign with no phantoms, the math is identical.
If your campaign was reading 40/40 before the fix because of 40 phantom rows, post-fix it reads 0/40 immediately. Real ramp budget is intact: phantoms cost no LinkedIn-side capacity, so freeing the cap mathematically restores the actual day's budget without raising LinkedIn risk.
Observability and what's coming
Every InMail send now emits a structured log line that ops can grep in real-time:
- [InMail-Extract] outcome=success_with_chatid (1% sample) or outcome=no_chatid_silent_drop (always logged) — tells us at extraction time whether a real chat_id came back.
- [Cap-Calc] campaign=X inmail_count_total=Y inmail_count_phantom_excluded=Z effective_count=W cap=N — tells us how the cap math is composing for your specific campaign.
If you're seeing any of these patterns and want a manual reconcile (we can heal your account-level counter to truth), reply to your support thread and we'll attach the LL#291 reconciler to your account. The reconciler is run manually after the extractor + cap-exclusion fixes are verified producing real chatIds, and is documented as the one-time exception to our normal RAISE-only counter discipline.
Frequently asked questions
Q: Why didn't I see a chatId-failed alert before today?
A: Pre-May-2 we wrote success-shape events on every Unipile 200-OK regardless of chat_id. The chatId gate is new — it landed in the morning of May 2, 2026. The afternoon V2 fix made the extractor read the right field so real chatIds flow.
Q: Are my refunds for the phantom period automatic?
A: Refund eligibility is reviewed manually for the 18-day phantom-affected period. If you sent InMails between mid-April 2026 and May 2, 2026 and consumed Sales Nav credits without messages reaching recipients, contact support with your account email and we'll review.
Q: Will this happen again?
A: We added two CI guards alongside the fix: a contract test that pins the chat_id-first extraction in sendInMail, and a source-grep guard that fails the build if any future POST to a chats-endpoint reads response.id without also accepting response.chat_id. The same pattern caught two prior families of regression (LL#286 inline-pause writers, LL#276 inline-status writers) — defense in depth is now standard for every Unipile field-name contract.
Q: What's the long-term plan for verifying delivery?
A: We're extending the chatId gate to send_invite and send_message paths over the next deploy. Same response-shape audit, same proof-of-delivery contract. The eventual invariant: zero events on the dashboard that don't correspond to a real LinkedIn-side conversation or relationship change.