# Send Missed Call Leads to Google Sheets

> How to send missed call leads to Google Sheets: CSV import, a real-time Google Apps Script doPost receiver that de-duplicates by call_sid, a relay for signature checks, and when to use a CRM instead.

Source: https://edesy.in/blog/missed-call-leads-to-google-sheets

**Short answer:** there are three ways to get missed call leads into Google Sheets:

1. **CSV import** (free, no code, not real time).
2. **Real-time webhook into a Google Apps Script web app** that appends `from`, `to`, timestamp
   and `call_sid`, de-duplicating by `call_sid`. Add a small relay in front if you also want
   the signature checked, because Apps Script cannot read request headers.
3. **Skip the sheet** and send leads to [Edesy CRM](https://edesy.in/crm), which accepts the webhook directly (free
   plan).

Prefer not to code it? **We set it up for you on request.**

[Get a Missed Call Number](https://missedcall.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=missed-call)

## Which Option to Choose

| Option | Real time | Code | Signature checked | Cost on top of rental |
|---|---|---|---|---|
| CSV export + import | No | None | n/a | None |
| Apps Script + relay | Yes | ~60 lines | Yes | ₹0.30 per event (or Unlimited) |
| Apps Script with secret in URL | Yes | ~40 lines | No (weaker) | ₹0.30 per event (or Unlimited) |
| Edesy CRM | Yes | None | Token URL | ₹0.30 per event (or Unlimited); CRM free plan |
| Done for you | Yes | None | Yes | On request |

## Option 1: CSV Import (No Code)

1. In the missed call portal, filter the log by number and date range.
2. Click **Export CSV** (up to 1M rows).
3. In Google Sheets: **File > Import > Upload**, choose "Append to current sheet".

Good for daily lists. Without a webhook, pay as you go charges only the ₹499 monthly number
rental (plus GST).

## Option 2: Real-Time with Google Apps Script

### What the webhook sends

Each missed call produces a JSON POST like this:

```json
{
  "event": "call.missed",
  "timestamp": "2026-10-20T09:42:18Z",
  "data": {
    "call_sid": "a1b2c3...",
    "from": "+9198XXXXXXXX",
    "to": "+9180XXXXXXXX",
    "direction": "inbound",
    "status": "missed",
    "phone_number_id": "5e229f03-...",
    "phone_number_name": "Hoarding MG Road"
  }
}
```

The body is signed with HMAC-SHA256 using your webhook secret, in the header
`X-Edesy-Signature: sha256=<hex>`. Failed deliveries are retried up to 6 times over about 15
minutes with the **same `call_sid`**. Full details are in the [missed call docs](https://edesy.in/docs/missed-call).

### Two Apps Script quirks you must know

- **No request headers.** A web app's `doPost(e)` gets the body and query parameters, not headers,
  so it cannot read `X-Edesy-Signature`.
- **It answers with a redirect.** Apps Script web apps reply to a POST with a 302 redirect to
  `script.googleusercontent.com`. Edesy's webhook sender refuses redirects in general (a redirect
  could point at an internal address) but follows this one Apps Script hop, so pointing the
  webhook straight at the `/exec` URL shows up as delivered. A shortened or custom-domain link to
  the script redirects somewhere else first and is not followed, so always paste the `/exec` URL.

To verify the signature as well, put a tiny relay in front that checks it, forwards the event to
Apps Script, and returns 200 to Edesy. A free serverless worker is enough.

### Step 1: Create the sheet and script

1. Create a Google Sheet with a tab named `Leads` and headers in row 1:
   `received_at | occurred_at | from | to | number_name | call_sid`
2. Open **Extensions > Apps Script** and paste:

```javascript
const SHEET_NAME = 'Leads';
const SHARED_KEY = 'replace-with-a-long-random-string';

function doPost(e) {
  if (!e || !e.parameter || e.parameter.key !== SHARED_KEY) {
    return json({ ok: false, error: 'unauthorised' });
  }

  let body;
  try {
    body = JSON.parse(e.postData.contents);
  } catch (err) {
    return json({ ok: false, error: 'bad json' });
  }
  if (body.event !== 'call.missed' || !body.data) {
    return json({ ok: true, ignored: true });
  }

  const d = body.data;
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  try {
    const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
    const seen = sheet.getRange('F:F')
      .createTextFinder(String(d.call_sid))
      .matchEntireCell(true)
      .findNext();
    if (seen) return json({ ok: true, duplicate: true });

    sheet.appendRow([
      new Date(),
      body.timestamp || '',
      d.from || '',
      d.to || '',
      d.phone_number_name || '',
      d.call_sid || '',
    ]);
  } finally {
    lock.releaseLock();
  }
  return json({ ok: true });
}

function json(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}
```

The lock stops two simultaneous calls writing at once, and the `call_sid` lookup makes retries and
replays safe.

### Step 2: Deploy as a web app

1. Click **Deploy > New deployment**, type **Web app**.
2. Execute as **Me**; who has access: **Anyone**.
3. Authorise, then copy the web app URL (ending in `/exec`).
4. Your forward URL is `<web app URL>?key=<SHARED_KEY>`.

If you edit the script later, deploy a new version of the same deployment so the URL stays the
same.

### Step 3: Add a signature-checking relay

This example runs on any serverless worker that supports the standard Fetch and Web Crypto APIs
(for example a free Cloudflare Worker). Set two secrets: `EDESY_SECRET` (your webhook signing
secret) and `SHEET_URL` (the forward URL from step 2).

```javascript
export default {
  async fetch(request, env) {
    if (request.method !== 'POST') return new Response('ok');
    const raw = await request.text();

    const expected = await sign(env.EDESY_SECRET, raw);
    const got = request.headers.get('X-Edesy-Signature') || '';
    if (!timingSafeEqual(expected, got)) {
      return new Response('bad signature', { status: 401 });
    }

    const res = await fetch(env.SHEET_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: raw,
      redirect: 'follow',
    });
    return new Response(res.ok ? 'ok' : 'sheet error', { status: res.ok ? 200 : 502 });
  },
};

async function sign(secret, body) {
  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));
  const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
  return 'sha256=' + hex;
}

function timingSafeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}
```

The relay follows Apps Script's redirect itself and returns a clean 200 to Edesy, so the delivery
log shows success. If the sheet is unreachable it returns 502 and Edesy retries.

Check your signature code against a sample payload with the free
[webhook signature verifier](https://edesy.in/tools/webhook-signature-verifier).

### Step 4: Connect it in the missed call portal

1. Open **Webhooks** and add the relay URL.
2. Subscribe to `call.missed` and set the signing secret (the same value as `EDESY_SECRET`).
3. Click **Send test event** and check the sheet.
4. Dial your missed call number and confirm a real row appears.

### The weaker shortcut: secret in the URL only

You can skip the relay and rely only on the `?key=` query parameter. It keeps casual traffic out,
but the key travels in the URL and may appear in logs, and nothing proves the event came from
Edesy. Use it only for low-stakes tests or internal sheets.

## Option 3: Use a CRM Instead

Sheets work well for one person and a few hundred leads. They get awkward when several people
follow up, when you need status and notes, or when rows run into thousands.

[Edesy CRM](https://edesy.in/crm) accepts the missed call webhook directly:

1. Add a **Custom webhook** lead source in the CRM and copy its URL.
2. Paste it into the missed call portal's webhooks page.
3. After the first event, map `data.from` to the phone field (a one-time step).

Every missed call then becomes a lead you can assign and track, on the free CRM plan. For
Salesforce, Zoho, HubSpot or LeadSquared, we connect it for you, usually within a few days, using
our webhook and API. See [missed call CRM integration](https://edesy.in/missed-call-service/crm-integration).

## Cost

| Volume | Plan | Monthly before GST |
|---|---|---|
| CSV only, any volume | Pay as you go | ₹499 |
| 1,000 webhook events | Pay as you go | ₹799 |
| 5,000 webhook events | Unlimited | ₹1,498 |

Add 18% GST. A one-time setup fee may apply per number. Above about 3,330 webhook events per
number a month, Unlimited is cheaper. See
[missed call service pricing in India](https://edesy.in/blog/missed-call-service-pricing-india).

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Delivery log shows 302 | Webhook URL is a short or custom-domain link, not the `/exec` URL | Paste the `script.google.com/.../exec` URL from Deploy |
| Relay returns 401 | Secret mismatch or body modified | Use the raw body; check the secret |
| Duplicate rows | `call_sid` column moved | Keep `call_sid` in column F or update the range |
| No rows, relay 200 | Wrong `key` in `SHEET_URL` | Copy the key exactly |
| Rows stop after edits | New deployment URL | Redeploy the same deployment |

## We Can Set It Up for You

If you would rather not touch code, tell us your sheet and we will set up the integration for your
account quickly.

[Get a Missed Call Number](https://missedcall.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=missed-call)

## Related Reading

- [How to set up a missed call campaign](https://edesy.in/blog/how-to-set-up-missed-call-campaign)
- [Missed call docs](https://edesy.in/docs/missed-call)
- [Missed call CRM integration](https://edesy.in/missed-call-service/crm-integration)
- [Edesy CRM](https://edesy.in/crm)

## Frequently Asked Questions

### Can missed call leads go to Google Sheets automatically?

Yes. Point the real-time call.missed webhook at a small Google Apps Script web app that appends each call as a row; Edesy follows the redirect Apps Script answers with, so deliveries show as delivered. Apps Script cannot read request headers, so add a tiny relay in front if you also want the signature checked. We can also set it up for you on request.

### Is there a no-code way to get missed calls into a sheet?

Yes. Export the CSV from the missed call dashboard and import it into Google Sheets. This is free on pay as you go without a webhook, but it is not real time.

### Why can't Apps Script verify the webhook signature?

A Google Apps Script web app does not expose incoming request headers to doPost, so it cannot read the X-Edesy-Signature header. A shared secret in the URL is a weaker substitute. A relay that checks the HMAC signature and then forwards to Apps Script closes that gap.

### How do I stop duplicate rows?

Every event carries a call_sid that stays the same on retries and replays. Check whether the call_sid is already in the sheet before appending.

### When should I use a CRM instead of a sheet?

When more than one person follows up, when you need assignment, status and notes, or when volume grows past a few thousand rows a month. Edesy CRM accepts the missed call webhook directly and has a free plan.
