Articles on: Leads

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


Notes: Always verify the signature using the unmodified raw request body


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-Signature header



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: true on the same flat lead object
  • Treat is_test: true as 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

Was this article helpful?

Share your feedback

Cancel

Thank you!