EN
Unipile MCP · Unified inbox

Build a unified inbox with a coding agent: LinkedIn, WhatsApp and email

One list of conversations across the accounts your users connected: LinkedIn and WhatsApp chats, Gmail and Outlook threads, with reply from the same screen. The Unipile API unifies the shape of the objects, and your app merges one call per account. With the Unipile MCP server, your coding agent reads the chat and email contracts and writes the fetch, the merge and the pagination in your stack. Part of the Unipile MCP server.
GET /v2/{account_id}/chats
GET /v2/{account_id}/emails
One cursor per account
Reply on the chat's own account
One call per account, one shared item shape, one cursor per account. 7-day free trial, no credit card.
Your agent · support-console
Unipile MCP connected
Sarah
Build a unified LinkedIn, WhatsApp and email inbox: list the connected accounts, fetch one page of chats and one page of emails per account, normalise them into one item shape and sort by date.
Run requestGET /v2/accounts/3 accounts
Added GET /api/inbox: it lists the accounts, calls the chats and emails routes in parallel per account, maps everything to { id, account_id, channel, from, preview, date } and sorts by date. Reply goes through the chat's own account.
3 files changed · 3 channels in one list
Now add load more, one cursor per account.
Read endpointGET /v2/{account_id}/chatsnext_cursor
Writing the cursor map
Describe the next feature…
The job

What you are trying to do

Show your users every conversation from the accounts they connected in one list, and let them reply without leaving your product. The honest part first: account_id sits in the path of every route, so a unified inbox is one call per account followed by a merge in your application. What the API unifies is the shape of the objects, not the number of calls.
Without it One client per provider, one shape each
A LinkedIn client, a WhatsApp client and an email client, each with its own model and its own pagination.
A single "load more" that loses messages as soon as one account runs out of pages before the others.
A reply that goes out on the wrong account because the chat and the account were not kept together.
With the Unipile MCP server The result in your application
One list of conversations across LinkedIn, WhatsApp and email, sorted by date, with the channel as a badge.
Load more that advances one cursor per account and never loses a message.
Reply from the list, on the account that owns the chat, tested on your Development application.
Prompt, steps, code

Give your agent the prompt

Two prompts, each with the Unipile endpoints the agent reads and runs and what lands in your project. Paths are complete, on the API base URL https://api.unipile.com, with your scoped key in the X-API-KEY header.
Your agent · support-consoleDevelopment app
Build a unified LinkedIn, WhatsApp and email inbox in this application: list the connected accounts, fetch one page of chats and one page of emails per account, normalise them into a single item shape, and sort by date.
Run requestGET /v2/accounts/3 accounts
Run requestGET /v2/{account_id}/chats200 · next_cursor
Added GET /api/inbox: lists accounts, fans out GET /v2/{account_id}/chats and GET /v2/{account_id}/emails in parallel, maps both to one InboxItem (id, account_id, channel, counterpart, preview, date, unread) and sorts by date. Opening an item calls the messages route of that chat. Ran it on the Development app with a LinkedIn, a WhatsApp and a Gmail test account.4 files changed · tested on the Development app

One call per account, one item shape, one sort

The agent reads the Chat and the Email schemas through the server and decides the minimal common shape: an identifier, a date, a sender, a preview and the channel. Everything provider-specific stays available on the raw object for the detail view. Instagram and Telegram come through the same chat routes, so a fourth channel is one more account, not one more client.
Endpoints the agent uses
GET/v2/accounts/GET/v2/{account_id}/chatsGET/v2/{account_id}/emailsGET/v2/{account_id}/chats/{chat_id}/messagesPOST/v2/{account_id}/chats/{chat_id}/messages/sendPOST/v2/{account_id}/emails/send
Common mistake: Looking for an account_id query filter. It is in the path: one call per account, then merge in your application.
Build the LinkedIn side in detail
Your agent · support-consoleDevelopment app
Add pagination to the unified inbox: keep one cursor per connected account, advance each one independently on load more, and stop an account when its next_cursor is absent.
Read endpointGET /v2/{account_id}/chatsdata, total_count, next_cursor
Run requestGET /v2/{account_id}/emails?cursor=…200 OK
Replaced the global offset by a Map<account_id, next_cursor> kept in the inbox state. Load more advances every account that still has a cursor, in parallel, then re-sorts the merged list. An account with no next_cursor is marked exhausted and skipped. Verified with three accounts of different sizes on the Development app.2 files changed · no message lost on load more

The envelope is the same everywhere: data, total_count, next_cursor

Every list route returns the same envelope. Pass next_cursor back in the cursor parameter to get the next page. The contract says to use the cursor when the provider supports it and offset otherwise, and that limit is a ceiling, not a guarantee: a short page is not the end of the list, only a missing next_cursor is.
Endpoints the agent uses
GET/v2/{account_id}/chatsGET/v2/{account_id}/emailsGET/v2/{account_id}/chats/{chat_id}/participants
Common mistake: One cursor for the whole inbox. Each account paginates on its own cursor; a shared one loses messages as soon as one account ends before the others.
Keep the list live with webhooks
Pagination

One envelope, one cursor per account

Verbatim from the v2 contract the agent reads through the server. The same three fields come back on every list route.
1The envelopedata holds the page, total_count the size when the provider gives it, next_cursor the token for the next page. Absent next_cursor means the end of that account's list.GET https://api.unipile.com/v2/{account_id}/chats?limit=20{ "object": "ChatList", "items": [ … ], "cursor": "…" }
2Cursor or offsetUse next_cursor whenever the provider supports it, offset otherwise. Code that assumes one of the two for every provider breaks on the first IMAP mailbox.GET https://api.unipile.com/v2/{account_id}/emails?cursor=…&limit=20GET https://api.unipile.com/v2/{account_id}/chats?offset=40&limit=20
3The cursor mapOne entry per account in your application state. Load more advances every account that still has a cursor and drops the ones that returned none.{ "acc_1a…": "eyJ…", "acc_9c…": null, "acc_f2…": "eyJ…" }limit is a ceiling, not a guarantee
Development to production

Test on a Development application first

Your Unipile dashboard separates a Development application from Production. Give the agent a scoped key from Development and one test account per channel: real page sizes, no real customer.
1Fetch one page per accountLinkedIn, WhatsApp and an email account, merged into one list sorted by date.
2Reply from the listThe send call goes out on the account that owns the chat.
3Load more with uneven accountsNo message lost, exhausted accounts skipped, then switch the key to Production.
crm-app · DevelopmentUsed by your agent
Scopedev-tests · 2 accounts
Keyscoped Account API key
AccountsLinkedIn test account, Gmail test mailbox
Webhooks1 endpoint · message events
crm-app · ProductionUntouched
Scopeone per workspace
Keyscoped keys, in your backend only
Accountsyour users' own accounts, via Hosted Auth
Troubleshooting

Common errors and what they mean

The four mistakes that break a unified inbox, and the fix for each. Three of them are about pagination.
Looking for account_id as a query filter
A single call is expected to return every account. Fixaccount_id is in the path. Call once per account and merge in your application; the API unifies the shape, not the number of calls.
One cursor for the whole inbox
Load more drops messages once an account ends before the others. FixKeep a map from account_id to next_cursor. Advance each account independently and stop the ones that returned no cursor.
Mixing cursor and offset
The code works on one provider and breaks on another. FixUse next_cursor when the provider supports it and offset otherwise, as the contract says. Read the envelope of each account instead of assuming.
Treating limit as a guarantee
A short page is read as the end of the list. Fixlimit is a ceiling. Only a missing next_cursor ends an account's list; a page with fewer items than requested does not.

Unified inbox FAQ

One call per account, the endpoints you need, pagination across accounts, message and email shapes, and how to keep the list live.
No, by design. account_id is part of the path, so you call once per account and merge in your application. What the API unifies is the shape of the objects, not the number of calls.
GET /v2/accounts/ for the list of accounts, then GET /v2/{account_id}/chats and GET /v2/{account_id}/emails per account, then GET /v2/{account_id}/chats/{chat_id}/messages to open a conversation. Replies go through POST /v2/{account_id}/chats/{chat_id}/messages/send and POST /v2/{account_id}/emails/send.
One cursor per account. Each list route returns data, total_count and next_cursor. Pass next_cursor back in the cursor parameter and keep a map of cursors, one per account, in your application state.
Messaging conversations are Chat objects and emails are Email objects, each with its own fields. The normalisation happens in your application on at least three fields: identifier, date and sender. The agent reads both schemas through the server and writes that mapping.
With a webhook endpoint subscribed to message.new and email.new. The dedicated page shows how an agent wires it.