Send WhatsApp from Google Sheets with Apps Script (step by step)
To send WhatsApp from Google Sheets, open Extensions > Apps Script, store the API key and secret in Script properties and use UrlFetchApp.fetch to call POST /api/v1/{instanceId}/send/text for each row. The script below reads Name, Phone and Message, sends, writes OK or the error in the Status column and skips rows already done. It also shows the in-sheet menu and how to receive replies by webhook.
Published on · 6 min read
What you need and how to lay out the sheet
A ZapZap account, an instance connected by QR code, the API key and secret (x-api-key and x-api-secret) and the instance ID. The sheet needs four columns on the first tab, in this order: A Name, B Phone, C Message, D Status.
The phone goes with country code and digits only, for example 5511999999999. If the column is formatted as a number, Sheets may turn the value into scientific notation or drop zeros; format column B as Plain text. Column D starts empty: the script writes each send's result there, and that is what prevents double sends. The short version of this walkthrough is also on the Google Sheets integration page.
How to store the key and secret without writing them in the code
Never paste credentials in the sheet's code: anyone with edit access sees everything. In the Apps Script editor, open Project settings (the gear) and, under Script properties, add ZAPZAP_KEY, ZAPZAP_SECRET and ZAPZAP_INSTANCE. The code reads those values with PropertiesService.
Mind who has access: the script owner and anyone with edit permission on the sheet can open the editor and read the properties. Share the sheet as Viewer with people who only need to see the data, and use a separate instance for spreadsheet use, not the business's main number.
How the script reads the sheet and calls the API
The code below walks the rows from the second one, skips those with a Status already filled, sends the message with UrlFetchApp.fetch and writes the result in column D. The muteHttpExceptions option stops an HTTP error from interrupting the script: you read the response code and decide what to write.
Utilities.sleep between sends creates an interval. The value is your choice: there is no official safe number from WhatsApp, and an identical burst to a cold list is the behavior that weighs the most. See the anti-ban guide before raising volume.
Apps Script
function enviarMensagens() {
const props = PropertiesService.getScriptProperties();
const key = props.getProperty("ZAPZAP_KEY");
const secret = props.getProperty("ZAPZAP_SECRET");
const instancia = props.getProperty("ZAPZAP_INSTANCE");
const url = "https://api.zapzapapi.com/api/v1/" + instancia + "/send/text";
const aba = SpreadsheetApp.getActiveSheet();
const linhas = aba.getDataRange().getValues();
for (let i = 1; i < linhas.length; i++) {
const [nome, telefone, mensagem, status] = linhas[i];
if (status || !telefone || !mensagem) continue;
const texto = String(mensagem).replace("{nome}", nome);
const resposta = UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: { "x-api-key": key, "x-api-secret": secret },
payload: JSON.stringify({ number: String(telefone), text: texto }),
muteHttpExceptions: true,
});
const codigo = resposta.getResponseCode();
aba.getRange(i + 1, 4).setValue(
codigo < 300 ? "OK " + new Date().toLocaleString("pt-BR") : "ERROR " + codigo + ": " + resposta.getContentText().slice(0, 120)
);
SpreadsheetApp.flush();
Utilities.sleep(3000 + Math.floor(Math.random() * 4000));
}
}How to add a menu inside the sheet to send with one click
With the onOpen function, Sheets gets a WhatsApp menu when the file opens. Whoever runs the sheet does not need to open the code editor. On the first run Google asks you to authorize the script to access the sheet and make external requests; that is expected.
Also add a test function that sends only to your own number, to validate credentials and the instance before you run the whole list.
Apps Script
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("WhatsApp")
.addItem("Enviar pendentes", "enviarMensagens")
.addItem("Enviar teste para mim", "enviarTeste")
.addToUi();
}
function enviarTeste() {
const props = PropertiesService.getScriptProperties();
const url = "https://api.zapzapapi.com/api/v1/" + props.getProperty("ZAPZAP_INSTANCE") + "/send/text";
const r = UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: { "x-api-key": props.getProperty("ZAPZAP_KEY"), "x-api-secret": props.getProperty("ZAPZAP_SECRET") },
payload: JSON.stringify({ number: props.getProperty("MEU_NUMERO"), text: "Test from Google Sheets" }),
muteHttpExceptions: true,
});
SpreadsheetApp.getUi().alert("Response " + r.getResponseCode() + ": " + r.getContentText());
}How to send a PDF or image from a sheet column
Add a column E with the file's public URL and use /send/media with type, file, text (caption) and docName. The file has to be reachable by public URL; a Drive link only works if it is open to anyone with the link and points to the file itself, not the viewer page.
For larger lists, consider the API's own sending queue (POST /sender/simple), which takes an array of numbers, the type, the text and a minimum and maximum interval between messages (delayMin and delayMax) and schedules the sending on the server. That way your script does not have to keep running, which also avoids Apps Script's execution time limit (check the current quotas in Google's documentation).
Apps Script
function enviarDocumento(numero, urlArquivo, nome, legenda) {
const props = PropertiesService.getScriptProperties();
const url = "https://api.zapzapapi.com/api/v1/" + props.getProperty("ZAPZAP_INSTANCE") + "/send/media";
return UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: { "x-api-key": props.getProperty("ZAPZAP_KEY"), "x-api-secret": props.getProperty("ZAPZAP_SECRET") },
payload: JSON.stringify({ number: numero, type: "document", file: urlArquivo, docName: nome, text: legenda }),
muteHttpExceptions: true,
});
}How to receive customers' replies in the sheet itself
Publish the script as a web app (Deploy > New deployment > Web app) and use the generated URL as the instance's webhook. The doPost function receives the event JSON and writes a row to a Replies tab. Configure the webhook with POST to /webhook, sending url, enabled true and events with messages.
Two cautions. First, check the exact event structure in a real log: write the raw body in the first version and map fields only afterward. Second, Apps Script as a web app has limits on time and concurrent executions; it suits low to medium volume and does not replace a server for a large operation. The webhook guide explains the design for higher volume.
Apps Script
function doPost(e) {
const evento = JSON.parse(e.postData.contents);
const aba = SpreadsheetApp.getActive().getSheetByName("Respostas") || SpreadsheetApp.getActive().insertSheet("Respostas");
aba.appendRow([new Date(), JSON.stringify(evento).slice(0, 4000)]);
return ContentService.createTextOutput("ok");
}cURL (registrar o webhook)
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/webhook \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"url":"https://script.google.com/macros/s/SEU_ID/exec","enabled":true,"events":["messages"]}'Which errors show up and what to do
| Symptom | Likely cause | Fix |
|---|---|---|
| ERROR 401 or 403 in the Status column | Wrong script properties or stray spaces | Reopen the properties and check the values |
| Error 400 | Phone without country code, with symbols or empty | Format column B as text and clean the number |
| Authorization exception | First run without permission | Authorize the script when Google asks |
| Script stops midway | Execution time limit | Send in batches or use the POST /sender/simple queue |
| Nothing reaches WhatsApp, status OK | Disconnected instance | Check GET /instance/status and reconnect by QR code |
| Webhook does not record | Old deployment or access not set to Anyone | Create a new deployment as a Web app with access for anyone |
Before any investigation, run the menu test. If it answers 200, credentials and instance are correct, and the problem is in the sheet data.
Good practices for using a spreadsheet as a sending panel
A spreadsheet is great for starting out and for small operations. Always record the result per row (for auditing and to avoid resending), use a number dedicated to the sheet, write messages with variation instead of identical text for the whole list and give people an easy way out. Messaging people who did not ask to be contacted is a problem in any tool, and data protection law applies equally.
When the list or the logic grows, move the same idea to n8n or code, keeping the API the same. The send/text reference and the quick start help with the next step.
Quick summary
- In Apps Script, UrlFetchApp.fetch calls POST /send/text with x-api-key and x-api-secret in headers and JSON in payload.
- Store the key and secret in Script properties, never in the sheet's code.
- Write the result in the Status column and skip rows already sent to avoid duplicates.
- For big lists, use the POST /sender/simple queue instead of keeping the script running.
- Receive replies by publishing the script as a Web app and registering its URL in POST /webhook.
Keep reading
Also read
Frequently asked questions
Do I have to pay Google anything to use Apps Script?
Apps Script is a feature of Google Workspace and personal accounts, with usage quotas that vary by account type. Check the current quotas in Google's documentation before planning high volume.
How do I send to a large list without the script timing out?
Use the API's sending queue (POST /sender/simple) with the numbers in an array and a minimum and maximum interval between messages. The server schedules and sends, and the script only makes one call.
The phone shows up in scientific notation in the sheet. Now what?
Format the phone column as Plain text before pasting the numbers and keep country code and area code, digits only. In the script, convert with String(phone).
Can I receive the customer's reply in the sheet?
Yes. Publish the script as a Web app with a doPost function, register the URL in POST /webhook with the messages event and write the received body to a tab. Check the event format in a real log before mapping columns.
Is it safe to leave the API key in the sheet?
Keep it in Script properties, not in cells or code. Even so, anyone with edit access can read them. Limit who edits, use a dedicated instance and generate new credentials if someone leaves the project.
Turn your spreadsheet into a WhatsApp channel
Create your account, verify your phone and get the welcome credit, no deposit needed (one per phone, subject to the current campaign). Connect by QR code and paste the script.
