> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.spreadly.app/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Send Leads with a 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)

```javascript
// /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 webhook = JSON.parse(rawBody);

  // Optional: handle test payloads (if enabled)
  if (webhook.data.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:
```json
{
  "type": "lead.created",
  "timestamp": "2025-10-22T19:51:54.000000Z",
  "data": {
    "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