Why the path is the API channel inbox
Chatwoot has ready-made channels for some providers, and D-API is not one of them. For those cases it offers the API channel inbox: a generic channel where an external system creates contacts, conversations and messages through the Chatwoot API, and Chatwoot notifies that system through a callback URL when an agent replies.
Neither side speaks the other’s language, so something has to translate. That something is the middleware, a small service you host. There is no native connector between D-API and Chatwoot, and the integration below is done entirely over HTTP and webhooks.
The path of each message
Customer writes on WhatsApp
- The customer messages the number connected on D-API.
- D-API fires
messages.receivedto the middleware’s inbound URL. - The middleware looks up the contact by phone number. If it does not exist, it creates the contact in the API inbox and stores the returned
source_id. - If there is no open conversation for that contact, it creates one and stores the ID.
- It creates the message in the conversation with
message_type: "incoming". The agent sees the message in the dashboard.
Agent replies in Chatwoot
- The agent writes in the conversation.
- Chatwoot calls the inbox callback URL with the
message_createdevent. - The middleware drops anything that is not a reply to the customer:
incomingmessages (the ones it created itself) and notes withprivateset to true. - Using the conversation ID, it finds the phone number and sends the text to D-API’s
/api/v1/messages/send/text.
Setting up both sides
In Chatwoot, go to Settings, Inboxes, Add Inbox and choose API. Enter a channel name and, in the webhook field, the middleware’s outbound URL. Then add the agents who will handle the inbox. To call the Chatwoot API, the middleware uses an access token generated in a user’s profile, sent in the api_access_token header.
On D-API, point the connection’s incoming message event to the middleware’s inbound URL:
curl -X POST https://api.d-api.cloud/api/v1/sessions/support/webhook-config \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "type": "single",
"events": { "messages.received": { "enabled": true, "webhookUrl": "https://middleware.yourcompany.com/inbound" } } }'The calls the middleware makes to Chatwoot, all with the token header:
| When | Chatwoot call |
|---|---|
| Customer’s first contact | POST /api/v1/accounts/ID/contacts with inbox_id, name and phone number |
| No open conversation | POST /api/v1/accounts/ID/conversations with source_id, inbox_id and contact_id |
| Each incoming message | POST /api/v1/accounts/ID/conversations/ID/messages with content and message_type incoming |
Outbound: from the Chatwoot callback to D-API
This is the part that raises the most questions, so it is worth seeing in code. The route below receives the inbox callback and replies to the customer. findConversation stands for your mapping table:
app.post('/outbound', async (req, res) => {
res.sendStatus(200)
const ev = req.body
if (ev.event !== 'message_created') return
if (ev.message_type !== 'outgoing' || ev.private) return
const link = await findConversation(ev.conversation.id)
if (!link) return // conversation that did not start on WhatsApp
await fetch('https://api.d-api.cloud/api/v1/messages/send/text', {
method: 'POST',
headers: { Authorization: process.env.DAPI_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: link.sessionId, to: link.phone, text: ev.content }),
signal: AbortSignal.timeout(10000),
})
})Note the message_type filter. Without it, every message the middleware creates as incoming fires the callback back and can turn into a duplicate send to the customer.
What the middleware needs to do well
- Persistent mapping: phone number to contact and conversation, conversation to phone number and
sessionId. In a database, not in memory, to survive deploys and run on more than one instance. - Idempotency: D-API resends the webhook when the URL fails or takes longer than 30 seconds. Use the message’s
data.idas the key and ignore what has already been logged. - Fast response: return 200 right away and process afterwards, or in a queue. Calling Chatwoot three times inside the webhook request is asking for a timeout.
- Messages from the phone itself: the received event also arrives with
fromMeset to true when someone writes from the device. Decide whether that goes into Chatwoot as a note or is ignored. - Dropped connection: also subscribe to
connection.statusto alert the team when the number disconnects, instead of finding out from a stalled queue.
When this design pays off
If your company wants a support desk without building screens, Chatwoot plus the WhatsApp API gets it done with a small service in the middle. With several numbers, one inbox per connection keeps each team on its own channel, as described in multiple WhatsApp numbers.
If you are building a support product to sell to other companies, the path is different: WhatsApp inside your own system, one connection per customer, as shown on the WhatsApp API for helpdesks and WhatsApp API for SaaS pages. To understand each event that reaches the middleware, see the WhatsApp webhooks guide.
