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 Method | Header | Used For |
|---|---|---|
| API Key | X-Org-Key: <key> | /api/public/* (lead creation) |
| JWT Token | Authorization: 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
- Log in to your Site2CRM dashboard
- Navigate to Settings in the sidebar
- Scroll to the API Access section
- 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.
| Endpoint | Limit | Window |
|---|---|---|
| POST /api/public/leads | 60 requests | 1 minute |
| POST /api/public/google-ads/leads | 60 requests | 1 minute |
| POST /api/webhooks/{id}/test | 10 requests | 1 minute |
| All other endpoints | 100 requests | 1 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
/api/public/leadsCreate a new lead in your organization. Automatically syncs to your connected CRM.
Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| X-Org-Key | string | Yes | Your organization API key |
| Content-Type | string | Yes | Must be application/json |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | Lead's email address | |
| name | string | No | Full name (or use first_name + last_name) |
| first_name | string | No | First name |
| last_name | string | No | Last name |
| phone | string | No | Phone number |
| company | string | No | Company name |
| source | string | No | Lead source (e.g., 'API', 'Landing Page') |
| notes | string | No | Additional notes |
| utm_source | string | No | UTM source parameter |
| utm_medium | string | No | UTM medium parameter |
| utm_campaign | string | No | UTM 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
/api/public/google-ads/leadsInbound 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| FULL_NAME | string | No | Full name |
| FIRST_NAME | string | No | First name |
| LAST_NAME | string | No | Last name |
| string | Yes | Email address (required) | |
| WORK_EMAIL | string | No | Work email (fallback if EMAIL missing) |
| PHONE_NUMBER | string | No | Phone number |
| WORK_PHONE | string | No | Work phone (fallback) |
| COMPANY_NAME | string | No | Company 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
| Event | Description |
|---|---|
| lead.created | A new lead is created (form, API, or chat) |
| lead.updated | An existing lead's fields are changed |
| form.submitted | A form submission is received |
| chat.started | An AI chat conversation begins |
| chat.lead_captured | AI 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| X-Site2CRM-Signature | string | Yes | HMAC-SHA256 signature for verifying authenticity |
| X-Site2CRM-Event | string | Yes | The event type (e.g. lead.created) |
| X-Site2CRM-Delivery | string | Yes | Unique delivery timestamp |
| Content-Type | string | Yes | Always application/json |
| User-Agent | string | Yes | Site2CRM-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.
/api/webhooksList all webhook subscriptions for your organization.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| event | string | No | Filter 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
}/api/webhooksCreate a new webhook subscription. Returns a secret for signature verification. Save it, it won't be shown again.
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | The URL to receive webhook POST requests |
| event | string | Yes | Event to subscribe to (e.g. lead.created) |
| description | string | No | Human-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.
/api/webhooks/{id}Get details for a specific webhook.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Webhook ID |
Returns the same shape as items in the list response. Returns 404 if not found or belongs to a different organization.
/api/webhooks/{id}Delete a webhook subscription. No more deliveries will be sent.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Webhook ID |
Returns 204 No Content on success. Returns 404 if not found.
/api/webhooks/eventsList 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" }
]/api/webhooks/{id}/deliveriesGet paginated delivery logs for a webhook. Useful for debugging failed deliveries.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Page number (default: 1) |
| page_size | integer | No | Items 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
}/api/webhooks/{id}/testSend a test payload to verify your webhook is receiving deliveries. Rate limited to 10/minute.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Webhook 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.
Request completed successfully. 201 for created resources, 204 for deletions.
Invalid request body or missing required fields.
Missing or invalid API key / JWT token.
Authenticated but lacking permission (e.g., no organization assigned).
The requested resource does not exist or belongs to a different organization.
Duplicate resource. For example, a webhook already exists for the same URL and event.
Rate limit exceeded. Check the Retry-After header.
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 "", 200Ready to integrate?
Sign up for Site2CRM to get your API key and start capturing leads programmatically.