Integrate with Webhooks
Requirement: A publicly reachable endpoint that accepts JSON POST requests.
Spreadly sends an HTTP POST with a JSON body containing a single lead object whenever a lead is created or updated. Authentication uses an HMAC SHA-256 signature over the raw request body.
Typical destinations:
- Automation platforms (Zapier, Make, n8n)
- Serverless (AWS Lambda, Vercel, Cloudflare)
- Custom APIs/middleware
Setup
1) In your Spreadly dashboard, go to: Leads → CRM Integrations → Webhook
2) Configure:
- Webhook URL: Your publicly accessible endpoint
- Secret Key: Copy and store securely (e.g.,
SPREADLY_WEBHOOK_SECRET)
3) Save your configuration
4) Send Test: Use the “Test webhook” button to verify your endpoint
Once enabled, Spreadly will POST the lead object on create and update.
Authentication
Signature-based (HMAC SHA-256, required):
- Header:
X-Spreadly-Signature - Contents: HMAC SHA-256 signature of the raw request body, computed with your Webhook Secret Key
Verification rules:
- Compute HMAC SHA-256 with
SPREADLY_WEBHOOK_SECRET - Use the raw request body bytes (before any JSON parsing)
- Compare exactly with
X-Spreadly-Signatureheader
Verification Example (Next.js/Node)
// /pages/api/spreadly-webhook.js
import crypto from 'crypto';
export const config = {
api: { bodyParser: false }, // important: use raw body for HMAC
};
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' });
}
const rawBody = await getRawBody(req);
const secret = process.env.SPREADLY_WEBHOOK_SECRET;
const signature = req.headers['x-spreadly-signature'];
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
if (!signature || signature !== expectedSignature) {
return res.status(401).json({ message: 'Invalid signature' });
}
// Body is a single lead object
const lead = JSON.parse(rawBody);
// Optional: handle test payloads (if enabled)
if (lead.is_test === true) {
return res.status(200).json({ message: 'Test webhook received successfully' });
}
// Upsert by lead.id
// await upsertLead(lead);
return res.status(200).json({ message: 'Webhook received successfully' });
}
function getRawBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
Payload Structure: Lead (Flat Object)
Example request body:
{
"id": 0,
"avatar_url": "https://localhost/img/assets/default_avatar.jpg",
"given_name": "John",
"family_name": "Doe",
"job_title": "Sales Manager",
"department": null,
"company": "Acme, Inc.",
"note": "This is a test note",
"emails": [
{ "id": 0, "email": "j.smith@acme.com" }
],
"phones": [
{ "id": 0, "number": "+39 58273 83572", "type": "mobile" }
],
"websites": [
{ "id": 0, "url": "https://acme.com" }
],
"addresses": [
{
"id": 0,
"line1": "Via Roma 12",
"line2": null,
"line3": null,
"city": "Roma",
"state": null,
"postal_code": null,
"country": "Italy"
}
],
"custom_fields": [],
"event_id": null,
"latitude": null,
"longitude": null,
"locale": "en",
"source": null,
"enriched_at": null,
"is_ai_enrichment_completed": true,
"external_id": null,
"external_system": null,
"external_url": null,
"created_at": "2025-10-22T19:51:54.000000Z",
"updated_at": "2025-10-22T19:51:54.000000Z"
}
Field notes:
- id: internal lead ID (stable across updates; use for upsert)
- given_name, family_name, job_title, department, company: profile fields
- note: optional free text
- emails[], phones[], websites[], addresses[]: arrays of related items
- custom_fields[]: extensibility for your schema
- event_id: optional event/capture reference
- latitude, longitude: optional geolocation
- locale: language/region code
- source: origin (e.g., spreadly_card, scanner_app)
- enrichment fields: enriched_at, is_ai_enrichment_completed
- external_*: links to external systems if applicable
- created_at, updated_at: ISO 8601 timestamps
Optional flags (if enabled in your workspace):
- ist_test: true can be included at the root to mark test payloads
Testing
- Go to Leads → CRM Integrations → Webhook and click “Test Webhook”
- Expect HTTP 200 if handled successfully
- If enabled, test payloads include
is_test: trueon the same flat lead object - Treat
is_test: trueas non-persistent and just return 200
Troubleshooting
Webhook not triggering:
- Ensure the endpoint is publicly accessible
- Return HTTP 200 on success
- Verify the webhook is enabled at Leads → CRM Integrations → Webhook
- Use “Test Webhook” to validate
Connection test failed:
- Accept POST
- Verify URL and availability
- Confirm server/function is running
- Review server logs
Updated on: 22/10/2025
Thank you!