Guide
Send completed jobs to a Google Sheet with a CalSpark webhook
CalSpark has no Google Sheets integration, and the Zapier app is planned and not available. What it has is a signed webhook for job.completed, which is off until an admin turns on API and Webhooks in Company, Features. This guide compares three ways to turn that webhook into a row, and shows the code for the one that checks the signature.
By CalSpark team. Published . Updated .
Set up the CalSpark side
- In Settings, Developers, Webhook endpoints, click Add endpoint.
- Enter your HTTPS receiver URL.
- Under Events to send, choose "Only selected events" and tick job.completed.
- Save, then copy the signing secret. An admin can show it again with Reveal secret on the endpoint page.
Then click Send test event. That sends a customer.updated sample about a made-up customer, so it will not look like a job. It proves the URL and the signature work. A real job.completed arrives when a job is marked completed.
What the event carries
This is trimmed from the example in the [event catalog](/developers#events).
{
"id": "evt_0000000000000000000000000000000c",
"type": "job.completed",
"data": {
"object": {
"id": "5081",
"object": "job",
"customer_id": "1042",
"scope_of_work": "Replace water heater",
"status": "completed",
"actual_start": "2026-10-08T14:05:00.000Z",
"actual_end": "2026-10-08T15:50:00.000Z",
"assigned_user_ids": ["usr_example"]
},
"previous_attributes": { "status": "in_progress" }
}
}The job has customer_id, not a name or an address. To get the name, call GET /api/v1/customers/{id} with an API key that has the customers:read scope, or leave the id in the sheet and look it up with a formula later. The public API allows 120 requests per minute per key.
Route A: a small function you host
This is the one route that can verify the signature. It needs a public HTTPS URL on port 443 and a Google Cloud service account.
Create a service account, enable the Sheets API, share the sheet with the service account email as an editor, and put the key JSON in an environment variable. Row one of a tab named Completed holds headers, with event id in column A. Reuse the verifier from the signature guide.
const express = require('express');
const { google } = require('googleapis');
const { verifyCalSpark } = require('./verify'); // from the signature guide
const app = express();
const auth = new google.auth.GoogleAuth({
credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
const spreadsheetId = process.env.SHEET_ID;
app.post('/calspark/jobs', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyCalSpark(raw, req.get('CalSpark-Signature'), [process.env.CALSPARK_WEBHOOK_SECRET])) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
if (event.type !== 'job.completed') return res.sendStatus(200);
const job = event.data.object;
try {
const seen = await sheets.spreadsheets.values.get({ spreadsheetId, range: 'Completed!A:A' });
if (!(seen.data.values || []).flat().includes(event.id)) {
await sheets.spreadsheets.values.append({
spreadsheetId,
range: 'Completed!A:H',
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
requestBody: {
values: [[event.id, job.id, job.customer_id, job.scope_of_work,
job.actual_start, job.actual_end, job.assigned_user_ids.join(' '), job.updated]],
},
});
}
res.sendStatus(200);
} catch (err) {
console.error(err);
res.sendStatus(500); // CalSpark will retry
}
});The append call finds the table in the range and adds a row after it. The check on column A is the dedupe. Two deliveries at the same instant could both pass it, which is rare at a small shop. Sheets allows 60 reads and 60 writes per minute per user, and this code makes one of each per event.
async function customerName(id) {
const r = await fetch('https://www.calspark.app/api/v1/customers/' + id, {
headers: { Authorization: 'Bearer ' + process.env.CALSPARK_API_KEY },
});
if (!r.ok) return 'customer ' + id;
const c = await r.json();
return c.company_name || [c.first_name, c.last_name].filter(Boolean).join(' ');
}Route B: a no-code tool with a webhook catch URL
Many automation tools give you a URL that accepts a POST and can add a spreadsheet row. You paste that URL into Add endpoint and map fields by clicking. There is no code and no server. Plainly: most of these tools cannot verify the CalSpark signature unless they offer a code step, so anyone who learns the URL could post fake jobs. Check the tool's own documentation, and keep the URL private. These tools also treat a 2xx as success, so they work with CalSpark's retry rules.
Route C: Google Apps Script, and why it is a poor fit
Apps Script can publish a web app that runs doPost. Google's web app guide lists the fields on the event object, and request headers are not among them. Without the CalSpark-Signature header you cannot verify anything.
The reply is also a problem. A Google developer write-up says a POST to the /exec URL first gets a 302 Found with a Location header. CalSpark does not follow redirects and counts any 3xx as a failure, so it would retry up to 8 times and could eventually disable the endpoint. I found no Google documentation that lets a script set the HTTP status, so I do not recommend this route.
Columns, a sample row and limits
| Column | Field | Sample |
|---|---|---|
| A | event id | evt_0000000000000000000000000000000c |
| B | job id | 5081 |
| C | customer | 1042, or the name from the lookup |
| D | scope_of_work | Replace water heater |
| E and F | actual_start, actual_end | 2026-10-08T14:05:00.000Z, 2026-10-08T15:50:00.000Z |
Deliveries are not guaranteed to be instant, and a failed one is retried on a schedule that stretches to 24 hours between attempts. Retries can deliver the same event twice, which is why column A matters. If your receiver is down for seven days straight, CalSpark disables the endpoint.
Sources
- Google Sheets API: Append values, checked October 6, 2026
- Google Sheets API: Basic writing, checked October 6, 2026
- Google Sheets API: Usage limits, checked October 6, 2026
- Google Apps Script: Web apps, checked October 6, 2026
- Google Workspace developers on DEV: POST to an Apps Script /exec URL, checked October 6, 2026
Questions
Is there a CalSpark Google Sheets integration?
No. There is no Google Sheets integration and no public Zapier app. Webhooks and the public API are what exist, and this guide uses them.
Can I get the customer name in the row?
Yes, with one extra call per event. The event carries customer_id only. Call GET /api/v1/customers/{id} with a key that has customers:read, or leave the id and look it up in the sheet.
Will I get the same job twice?
Possibly. Retries can send an event again with the same event id. Check column A before appending, as in the code above.
Does the test button send a job?
No. Send test event sends a customer.updated sample with a made-up customer. Complete a real job to see job.completed.
Can I use a spreadsheet instead of building this?
For one-time data, import is simpler. See [importing customers from a spreadsheet](/guides/import-customers-from-spreadsheet). An admin can also download customers, jobs and invoices as CSV from Settings, Data (see [exporting your data](/guides/export-your-data-csv-columns)).
Keep reading
- Verify a CalSpark Webhook Signature in Node and PythonReference for the CalSpark-Signature header: the signed string, 300 second tolerance, Node and Python code, secret rotation, retries, dedupe and disabling.
- Developer docs: REST API, webhooks and eventsThe CalSpark REST API: Bearer API keys, scopes, pagination, idempotency, signed webhooks and every event payload. Off until an admin enables it in Company, Features.
- Webhooks and API: setup, examples and limitsTurn on the API in Company, Features, then create API keys and add a signed webhook endpoint under Settings, Developers. A worked example and the limits.
- Zapier: plannedConnect CalSpark to thousands of other tools without code. Planned. Not available.
- How to import customers from a spreadsheetClean a customer spreadsheet, map columns, catch duplicates and test with ten rows before you load the rest into your field service software.
Try it with your own jobs
14-day free trial. No card. $49 a month flat for up to 10 users after that.