When the spreadsheet is the right tool
A lot of small operations live in Google Sheets: the class roster, customers with an installment coming due, event attendees. Sending WhatsApp messages to that list from the spreadsheet itself avoids exporting a CSV to another tool and keeps track of who already got the message in the same place.
There is no native integration between D-API and Google Sheets. The connection is made with the Apps Script that comes with every spreadsheet: it calls the WhatsApp API over HTTP and publishes a URL to receive the webhook. It works well for dozens or a few hundred sends per day. Beyond that, Apps Script limits start to get in the way, and we cover them below.
Set up the spreadsheet before the code
Create two tabs. The first, called Sends, with these columns in row 1:
| Column | Content | Example |
|---|---|---|
| A: phone | Digits only, with country code | 14155550123 |
| B: name | First name for personalization | Carla |
| C: message | Text to send | Your workshop registration is confirmed. |
| D: optin | YES when the person agreed | YES |
| E: status | Filled in by the script | sent |
| F: sent_at | Filled in by the script | date and time |
The second tab, Replies, receives what people write back. Then open Extensions, Apps Script and, under Project Settings, create the script properties DAPI_KEY with your API key and DAPI_SESSION with the connection ID.
Send with UrlFetchApp
The function below walks the Sends tab, skips anyone without opt-in or who already got the message, sends in small batches and records the result of each row. muteHttpExceptions turns an error into text in the spreadsheet instead of crashing the whole run:
const BATCH = 40 // fits comfortably within a 6-minute execution
function sendPending() {
const props = PropertiesService.getScriptProperties()
const sheet = SpreadsheetApp.getActive().getSheetByName('Sends')
const rows = sheet.getDataRange().getValues()
let sent = 0
for (let i = 1; i < rows.length && sent < BATCH; i++) {
const [phone, name, message, optin, status] = rows[i]
if (optin !== 'YES' || status) continue
const resp = UrlFetchApp.fetch('https://api.d-api.cloud/api/v1/messages/send/text', {
method: 'post',
contentType: 'application/json',
headers: { Authorization: props.getProperty('DAPI_KEY') },
payload: JSON.stringify({
sessionId: props.getProperty('DAPI_SESSION'),
to: String(phone),
text: 'Hi ' + name + '! ' + message,
}),
muteHttpExceptions: true,
})
const ok = resp.getResponseCode() < 300
sheet.getRange(i + 1, 5, 1, 2).setValues([[ok ? 'sent' : 'error ' + resp.getContentText(), new Date()]])
sent++
Utilities.sleep(4000 + Math.random() * 4000) // irregular interval between sends
}
}To run it automatically, create a time-driven trigger under Triggers that calls sendPending every 15 minutes. Each run takes the next batch of rows with no status, so a large list is worked through over the day without exceeding the time limit of a single run.
Receive replies with doPost
In the same project, add the function Google calls when someone POSTs to the Web App URL. It logs only messages received from contacts, outside groups, and ignores repeated IDs:
function doPost(e) {
const event = JSON.parse(e.postData.contents)
const d = event.data
if (event.event !== 'messages.received' || d.fromMe || d.is_group) return ok()
const cache = CacheService.getScriptCache()
if (cache.get(d.id)) return ok()
cache.put(d.id, '1', 21600) // 6 hours
SpreadsheetApp.getActive().getSheetByName('Replies').appendRow([
new Date(d.timestamp),
d.from.jid.split('@')[0],
d.from_name,
d.type === 'text' ? d.message : '[' + d.type + ']',
])
return ok()
}
function ok() {
return ContentService.createTextOutput('ok')
}Publish under Deploy, New deployment, type Web app, executing as you and with access for anyone, since D-API calls the URL without a Google login. Copy the URL ending in /exec and register it as the connection’s webhook:
curl -X POST https://api.d-api.cloud/api/v1/sessions/YOUR_SESSION/webhook \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "webhookUrl": "https://script.google.com/macros/s/YOUR_ID/exec" }'Keep the Web App URL private. To go further, check in doPost that the event’s sessionId matches your connection. The full event format is in the WhatsApp webhooks guide.
Apps Script quotas and volume
- Time per execution: 6 minutes. That is why sending happens in batches with a trigger, not in a loop over the whole spreadsheet.
- UrlFetch calls: on a personal account, 20,000 per day, counting everything your scripts do. Google Workspace accounts have a higher quota.
- Triggers: personal accounts have a daily limit on total trigger runtime. Long intervals between sends consume that time.
- Concurrent writes: if many replies arrive at once, parallel
appendRowcalls can slow down. At high volume, the spreadsheet stops being the right database.
The limit that matters most, though, is WhatsApp’s. Irregular intervals between sends, personalized messages and sending only to people who asked are what keep the number alive. The topic has its own guide: how to avoid bans.
When to move off the spreadsheet
If the list has grown past a few thousand contacts or the send needs to happen the second something changes in your system, the spreadsheet has become a bottleneck. For large, recurring sends, see bulk messaging with the API. For date-driven reminders, such as due dates, the payment reminders guide shows the design with the source system. And if you prefer visual, no-code flows, n8n combines the D-API node with the Google Sheets node it already ships with.
