Apps Script doPost and the Sume webhook signature: what you can verify

An Apps Script web app doPost documents the body but no headers, and Sume signs in headers. Treat the callback as a hint and re-read the job with your key.

5 min readSume
All posts

You cannot verify a Sume webhook signature from an Apps Script doPost(e) as documented, because Sume sends the signature in request headers and Google's web app guide lists only the body and query fields on the event object. The safe pattern is to treat the callback as a hint, take the job id from it, and read the real state from the Sume API with your own key.

Everything below about Apps Script comes from Google's web apps guide, read on 2026-10-02. If Google adds header access later, switch to a normal signature check.

What does doPost(e) give you?

The guide documents e.queryString, e.parameter, e.parameters, e.pathInfo, e.contextPath, e.contentLength and e.postData, with the raw text in e.postData.contents. It says the function must return an HTML Service or Content Service output. The guide does not document a headers field.

What does Sume put in the headers?

Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>; every delivery also carries x-sume-webhook-secret-fingerprint. The verifying webhooks page covers the scheme. All three values are headers, so a receiver that cannot read headers cannot run the check.

A signature is also not the only way to know a callback is real. Job webhooks name a job_id, and GET /v1/jobs/{id}/status answers with the true state for a key you hold.

How do I handle the callback safely anyway?

Never write the callback's own result into your sheet. Use it only to learn which job to look at, then fetch status yourself. A forged callback can then cause one extra read, nothing more. The code stores the key in Script Properties, ignores anything without a job_ id, and writes only what the API returns.

function doPost(e) {
  const body = JSON.parse(e.postData.contents || '{}');
  const jobId = String(body.job_id || '');
  if (!/^job_[A-Za-z0-9_]+$/.test(jobId)) return ContentService.createTextOutput('ignored');
  const key = PropertiesService.getScriptProperties().getProperty('SUME_API_KEY');
  const res = UrlFetchApp.fetch('https://api.sume.com/v1/jobs/' + jobId + '/status', {
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  });
  const status = JSON.parse(res.getContentText());
  const sheet = SpreadsheetApp.getActiveSheet();
  const ids = sheet.getRange(1, 2, sheet.getLastRow(), 1).getValues().flat();
  const row = ids.indexOf(jobId) + 1;
  if (row > 0 && status.terminal) sheet.getRange(row, 3).setValue(status.sume_status);
  return ContentService.createTextOutput('ok');
}

Why not just skip the signature?

A signature proves a delivery was signed for your workspace: Sume derives the secret per workspace, so a valid signature means the call came from Sume for you. Without it, anyone who learns your web app URL can post a body that names a job id. That is why the code above never trusts the body for anything except a lookup key.

The job id regex matters too. Spreadsheet code that builds a URL from unchecked input is an easy place to introduce a bug, so reject anything that does not look like a Sume job id before you build the request. Also remember that a callback only arrives for jobs submitted with a webhook_url; rows submitted with mode: "async" alone are found by the polling trigger as in Apps Script UrlFetch quotas and a Sume sheet run.

How do I test this receiver before real jobs?

Use Sume's Send test, which is a control on the dashboard Webhooks page or POST /v1/webhooks/test-deliveries. It posts a signed dummy webhook.test body to a URL you type and never replays a real job. That body has no job_id, so the receiver above should answer ignored and write nothing, which is exactly the behavior you want to confirm.

Then submit one cheap image with your web app URL as webhook_url, and read the job's events for the webhook.delivery entry. If it shows a failed attempt, check that the deployment is reachable without a Google sign-in, because Sume calls it as an anonymous public client.

What are the limits of this approach?

Sume retries a non-2xx or timed-out delivery up to 10 attempts total with a 10-second timeout per attempt, so return quickly. The URL must also be public HTTPS. Keep polling as a backup, as the webhook docs advise.

If you need a verified receiver, use a platform that exposes headers, such as the Hono or Express receivers in Hono webhook: verify the signature on the raw body.

Receiver options in Apps Script, read 2026-10-02
OptionVerifies signatureCost
doPost as a hint, then GET statusNo, but state comes from the APIOne UrlFetch per callback
Relay with a header-aware service, then call the web appYes, at the relayExtra hop to run
Poll only, no webhookNot neededUrlFetch calls against the 20,000 daily quota

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume