Developer Documentation

Site2CRM API

Integrate lead capture and webhook automation directly into your applications.

Overview

The Site2CRM API lets you programmatically create leads and manage webhooks. Use it to integrate lead capture from custom forms, landing pages, mobile apps, or third-party services, and receive real-time notifications when events happen in your account.

Base URL: https://api.site2crm.io

Authentication

Site2CRM uses two authentication methods depending on the API you're accessing.

Auth MethodHeaderUsed For
API KeyX-Org-Key: <key>/api/public/* (lead creation)
JWT TokenAuthorization: Bearer <jwt>/api/webhooks/* (webhook management)

API Key Authentication

Use your Organization API Key to create leads via the public API. Include it in theX-Org-Keyheader with every request.

Finding Your API Key

  1. Log in to your Site2CRM dashboard
  2. Navigate to Settings in the sidebar
  3. Scroll to the API Access section
  4. Copy your API key
curl -X POST https://api.site2crm.io/api/public/leads \
  -H "Content-Type: application/json" \
  -H "X-Org-Key: your_api_key_here" \
  -d '{"email": "lead@example.com", "name": "John Doe"}'

JWT Token Authentication

Webhook management endpoints require a JWT access token obtained by logging in viaPOST /api/auth/login. Include the token in the Authorization header.

curl https://api.site2crm.io/api/webhooks \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Security Warning: Never expose your API key or JWT token in client-side code. Always make API calls from your server to protect your credentials.

Rate Limits

API requests are rate limited to ensure fair usage and protect the service from abuse.

EndpointLimitWindow
POST /api/public/leads60 requests1 minute
POST /api/public/google-ads/leads60 requests1 minute
POST /api/webhooks/{id}/test10 requests1 minute
All other endpoints100 requests1 minute

Rate Limit Headers

When rate limited, the API returns a 429 Too Many Requests response with:

{
  "error": "Too many requests",
  "message": "Rate limit exceeded. Please try again in 45 seconds.",
  "retry_after": 45
}

The Retry-After header is also set with the number of seconds to wait.

Create Lead

POST/api/public/leads

Create a new lead in your organization. Automatically syncs to your connected CRM.

Headers

ParameterTypeRequiredDescription
X-Org-KeystringYesYour organization API key
Content-TypestringYesMust be application/json

Body Parameters

ParameterTypeRequiredDescription
emailstringYesLead's email address
namestringNoFull name (or use first_name + last_name)
first_namestringNoFirst name
last_namestringNoLast name
phonestringNoPhone number
companystringNoCompany name
sourcestringNoLead source (e.g., 'API', 'Landing Page')
notesstringNoAdditional notes
utm_sourcestringNoUTM source parameter
utm_mediumstringNoUTM medium parameter
utm_campaignstringNoUTM campaign parameter

Success Response (201)

{
  "message": "Lead received",
  "lead_id": 12345
}

Duplicate Lead Response (200)

If a lead with the same email was created in the last 24 hours, the existing lead is updated instead.

{
  "message": "Lead updated",
  "lead_id": 12345,
  "merged": true
}

Custom Fields

Any additional fields not listed above will be captured as custom fields and appended to the lead's notes.

{
  "email": "lead@example.com",
  "name": "John Doe",
  "budget": "$10,000",
  "timeline": "Q2 2026",
  "product_interest": "Enterprise Plan"
}

Google Ads Lead Form

POST/api/public/google-ads/leads

Inbound webhook for Google Ads lead form extensions. No auth header needed. Use google_key in the payload.

How It Works

Configure Google Ads to send lead form submissions to this URL. Include your API key as the google_key field in the webhook payload. Google expects an empty 200 response.

Payload Format

{
  "google_key": "your_api_key_here",
  "lead_id": "TeSter-abc123",
  "campaign_id": 12345678,
  "gcl_id": "CjwKCAjw...",
  "is_test": false,
  "user_column_data": [
    { "column_id": "FULL_NAME", "string_value": "Jane Smith" },
    { "column_id": "EMAIL", "string_value": "jane@acme.com" },
    { "column_id": "PHONE_NUMBER", "string_value": "+15551234567" },
    { "column_id": "COMPANY_NAME", "string_value": "Acme Corp" }
  ]
}

Supported Column IDs

ParameterTypeRequiredDescription
FULL_NAMEstringNoFull name
FIRST_NAMEstringNoFirst name
LAST_NAMEstringNoLast name
EMAILstringYesEmail address (required)
WORK_EMAILstringNoWork email (fallback if EMAIL missing)
PHONE_NUMBERstringNoPhone number
WORK_PHONEstringNoWork phone (fallback)
COMPANY_NAMEstringNoCompany name

Any additional column IDs are captured in the lead's notes. The lead source is automatically set to google_ads.

Webhooks Guide

Webhooks let you receive real-time HTTP notifications when events happen in your Site2CRM account. Instead of polling the API, register a webhook URL and we'll POST a JSON payload to it whenever the event fires.

Available Events

EventDescription
lead.createdA new lead is created (form, API, or chat)
lead.updatedAn existing lead's fields are changed
form.submittedA form submission is received
chat.startedAn AI chat conversation begins
chat.lead_capturedAI chat captures a visitor's email or phone

Payload Format

Every webhook delivery is a POST request with a JSON body:

{
  "event": "lead.created",
  "timestamp": "2026-02-25T14:30:00.000000",
  "data": {
    "lead_id": 12345,
    "email": "jane@acme.com",
    "name": "Jane Smith",
    "first_name": "Jane",
    "last_name": "Smith",
    "phone": "+15551234567",
    "company": "Acme Corp",
    "source": "form",
    "notes": null,
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "spring_sale",
    "created_at": "2026-02-25T14:30:00.000000"
  }
}

Webhook Headers

Each delivery includes these headers:

ParameterTypeRequiredDescription
X-Site2CRM-SignaturestringYesHMAC-SHA256 signature for verifying authenticity
X-Site2CRM-EventstringYesThe event type (e.g. lead.created)
X-Site2CRM-DeliverystringYesUnique delivery timestamp
Content-TypestringYesAlways application/json
User-AgentstringYesSite2CRM-Webhook/1.0

Verifying Signatures

When you create a webhook, a secret is returned in the response. Use it to verify that deliveries are genuine. The signature is an HMAC-SHA256 hex digest of the JSON payload (compact, sorted keys) prefixed with sha256=.

Python

import hmac, hashlib, json

def verify_signature(payload_bytes: bytes, secret: str, signature: str) -> bool:
    payload = json.loads(payload_bytes)
    canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True)
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js

const crypto = require("crypto");

function verifySignature(payloadString, secret, signature) {
  const payload = JSON.parse(payloadString);
  const canonical = JSON.stringify(payload, Object.keys(payload).sort());
  // Note: use compact JSON with sorted keys matching Python's separators=(",",":")
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(JSON.parse(payloadString), null, 0))
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

// Simpler approach: canonicalize the same way as the server
function verifySignatureCanonical(payloadString, secret, signature) {
  const obj = JSON.parse(payloadString);
  const canonical = JSON.stringify(obj, Object.keys(obj).sort());
  // Remove spaces after separators to match Python's separators=(",",":")
  const compact = canonical.replace(/: /g, ":").replace(/, /g, ",");
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(compact)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

Important: Always use constant-time comparison (e.g. hmac.compare_digest orcrypto.timingSafeEqual) to prevent timing attacks.

GET/api/webhooks

List all webhook subscriptions for your organization.

Query Parameters

ParameterTypeRequiredDescription
eventstringNoFilter by event type (e.g. lead.created)

Response (200)

{
  "items": [
    {
      "id": 1,
      "url": "https://example.com/webhook",
      "event": "lead.created",
      "description": "Slack notification",
      "is_active": true,
      "secret": null,
      "total_deliveries": 42,
      "successful_deliveries": 40,
      "failed_deliveries": 2,
      "last_delivery_at": "2026-02-25T14:30:00",
      "created_at": "2026-02-01T10:00:00",
      "updated_at": "2026-02-25T14:30:00"
    }
  ],
  "total": 1
}
POST/api/webhooks

Create a new webhook subscription. Returns a secret for signature verification. Save it, it won't be shown again.

Body Parameters

ParameterTypeRequiredDescription
urlstringYesThe URL to receive webhook POST requests
eventstringYesEvent to subscribe to (e.g. lead.created)
descriptionstringNoHuman-readable label for this webhook

Response (201)

{
  "id": 5,
  "url": "https://example.com/webhook",
  "event": "lead.created",
  "description": "Slack notification",
  "is_active": true,
  "secret": "abc123...xyz",
  "total_deliveries": 0,
  "successful_deliveries": 0,
  "failed_deliveries": 0,
  "last_delivery_at": null,
  "created_at": "2026-02-25T15:00:00",
  "updated_at": "2026-02-25T15:00:00"
}

Save the secret! The secret field is only returned when creating the webhook. Use it to verify delivery signatures.

GET/api/webhooks/{id}

Get details for a specific webhook.

Path Parameters

ParameterTypeRequiredDescription
idintegerYesWebhook ID

Returns the same shape as items in the list response. Returns 404 if not found or belongs to a different organization.

DELETE/api/webhooks/{id}

Delete a webhook subscription. No more deliveries will be sent.

Path Parameters

ParameterTypeRequiredDescription
idintegerYesWebhook ID

Returns 204 No Content on success. Returns 404 if not found.

GET/api/webhooks/events

List all available webhook event types. No authentication required.

Response (200)

[
  { "event": "lead.created", "description": "Fires when a new lead is created" },
  { "event": "lead.updated", "description": "Fires when a lead is updated" },
  { "event": "form.submitted", "description": "Fires when a form is submitted" },
  { "event": "chat.started", "description": "Fires when an AI chat conversation starts" },
  { "event": "chat.lead_captured", "description": "Fires when AI chat captures lead info" }
]
GET/api/webhooks/{id}/deliveries

Get paginated delivery logs for a webhook. Useful for debugging failed deliveries.

Query Parameters

ParameterTypeRequiredDescription
pageintegerNoPage number (default: 1)
page_sizeintegerNoItems per page (default: 20, max: 100)

Response (200)

{
  "items": [
    {
      "id": 101,
      "event": "lead.created",
      "payload": "{...}",
      "response_status": 200,
      "response_body": "OK",
      "error_message": null,
      "delivered_at": "2026-02-25T14:30:00",
      "duration_ms": 245,
      "attempt_number": 1,
      "is_success": true
    }
  ],
  "total": 42,
  "page": 1,
  "page_size": 20
}
POST/api/webhooks/{id}/test

Send a test payload to verify your webhook is receiving deliveries. Rate limited to 10/minute.

Path Parameters

ParameterTypeRequiredDescription
idintegerYesWebhook ID

Response (200)

{
  "success": true,
  "status_code": 200,
  "response_body": "OK",
  "error": null
}

The test payload includes "test": true so your handler can distinguish test deliveries from real ones.

Error Handling

The API uses standard HTTP status codes to indicate success or failure.

2xxSuccess

Request completed successfully. 201 for created resources, 204 for deletions.

400Bad Request

Invalid request body or missing required fields.

401Unauthorized

Missing or invalid API key / JWT token.

403Forbidden

Authenticated but lacking permission (e.g., no organization assigned).

404Not Found

The requested resource does not exist or belongs to a different organization.

409Conflict

Duplicate resource. For example, a webhook already exists for the same URL and event.

429Too Many Requests

Rate limit exceeded. Check the Retry-After header.

500Server Error

Something went wrong on our end. Please try again or contact support.

Error Response Format

{
  "detail": {
    "error": "validation_error",
    "message": "Email is required",
    "field": "email"
  }
}

Code Examples

Create a Lead (cURL)

curl -X POST https://api.site2crm.io/api/public/leads \
  -H "Content-Type: application/json" \
  -H "X-Org-Key: your_api_key_here" \
  -d '{
    "email": "jane@acme.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "company": "Acme Corp",
    "phone": "(555) 123-4567",
    "source": "API",
    "utm_source": "google",
    "utm_campaign": "spring_sale"
  }'

JavaScript (fetch)

const response = await fetch('https://api.site2crm.io/api/public/leads', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Org-Key': 'your_api_key_here',
  },
  body: JSON.stringify({
    email: 'jane@acme.com',
    first_name: 'Jane',
    last_name: 'Smith',
    company: 'Acme Corp',
    source: 'Website Form',
  }),
});

const data = await response.json();
console.log(data);

Python (requests)

import requests

response = requests.post(
    'https://api.site2crm.io/api/public/leads',
    headers={
        'Content-Type': 'application/json',
        'X-Org-Key': 'your_api_key_here',
    },
    json={
        'email': 'jane@acme.com',
        'first_name': 'Jane',
        'last_name': 'Smith',
        'company': 'Acme Corp',
        'source': 'Python Script',
    }
)

print(response.json())

PHP

<?php
$ch = curl_init('https://api.site2crm.io/api/public/leads');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-Org-Key: your_api_key_here',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'email' => 'jane@acme.com',
        'first_name' => 'Jane',
        'last_name' => 'Smith',
        'company' => 'Acme Corp',
        'source' => 'PHP Form',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));

Subscribe to a Webhook (cURL)

curl -X POST https://api.site2crm.io/api/webhooks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_jwt_token" \
  -d '{
    "url": "https://example.com/hooks/site2crm",
    "event": "lead.created",
    "description": "Slack new lead notification"
  }'

# Save the "secret" from the response for signature verification!

Verify Webhook Signature (Node.js)

const crypto = require("crypto");
const express = require("express");
const app = express();

app.post("/hooks/site2crm", express.json(), (req, res) => {
  const signature = req.headers["x-site2crm-signature"];
  const secret = process.env.WEBHOOK_SECRET;

  // Canonicalize: compact JSON with sorted keys
  const canonical = JSON.stringify(
    JSON.parse(JSON.stringify(req.body)),
    Object.keys(req.body).sort(),
  ).replace(/: /g, ":").replace(/, /g, ",");

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(canonical)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.status(401).send("Invalid signature");
  }

  console.log("Event:", req.headers["x-site2crm-event"]);
  console.log("Data:", req.body.data);
  res.sendStatus(200);
});

Verify Webhook Signature (Python / Flask)

import hmac, hashlib, json, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.route("/hooks/site2crm", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Site2CRM-Signature", "")
    secret = os.environ["WEBHOOK_SECRET"]

    payload = request.get_json(force=True)
    canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True)
    expected = "sha256=" + hmac.new(
        secret.encode(), canonical.encode(), hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(401, "Invalid signature")

    event = request.headers.get("X-Site2CRM-Event")
    print(f"Event: {event}, Data: {payload['data']}")
    return "", 200

Ready to integrate?

Sign up for Site2CRM to get your API key and start capturing leads programmatically.