Receive Lead via Webhook
Push a lead into Superfone by POSTing a signed JSON payload to your integration's webhook URL. The endpoint matches by phone number — if a lead with that phone already exists in your account, it's updated; otherwise a new lead is created and round-robin assigned.
The endpoint authenticates with HMAC-SHA256 signatures, returns 202 Accepted immediately, and processes the lead asynchronously. Get the webhook URL and signing secret from the integration's settings in the Superfone dashboard.
This endpoint requires a signed request. CORS is allowed, so browser calls won't be blocked at the network layer — but the signing secret must never be shipped to a browser. Make all requests from your backend. See Overview for the full security model.
HTTP Request
POST /v2/webhook/integration/:webhook_uuid
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
webhook_uuid | string | Yes | The unique identifier issued when you created the integration in the dashboard. |
Required Headers
| Header | Description |
|---|---|
content-type | Must be application/json. |
x-webhook-signature | signature=<hex> — HMAC-SHA256 of the signing string. See Signing. |
x-webhook-timestamp | Unix timestamp in seconds when the request was created. Must be within ±5 minutes of Superfone's clock. |
idempotency-key | A unique string per delivery (UUIDv4 recommended). See Idempotency. |
x-request-id | Optional. Client-supplied trace ID, echoed back on the response. |
Signing
The signature header value is:
x-webhook-signature: signature=<hex-digest>
where <hex-digest> is the lowercase hex-encoded HMAC-SHA256 of the signing string using your integration's signing secret as the key.
The signing string concatenates three values with . separators:
<x-webhook-timestamp>.<raw-request-body>.<idempotency-key>
A few critical details:
- Use the raw request body bytes — the exact bytes you send over the wire. Serialize your payload once and reuse the same string both for signing and as the request body. Re-serializing with different whitespace or key ordering will break the signature.
- Bind all three values. The timestamp and idempotency key are part of the signing string so an attacker cannot swap them independently.
- Constant-time compare. Superfone uses
crypto.timingSafeEqualfor verification on its side; you should do the same when verifying signatures elsewhere.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
customer_phone | string | Yes | Lead's phone number (E.164 preferred, e.g. +918000000001). Used to match an existing lead or create a new one. |
first_name | string | No | Lead's first name. Existing leads keep their stored first/last name if the incoming first_name looks like a phone number. |
last_name | string | No | Lead's last name. |
email | string | string[] | No | One or more email addresses. A single string is accepted and converted to a one-element array. |
address | string | object | No | Address. A plain string is stored as { "text": "..." }; an object is stored as-is. See Address object. |
website | string | No | Lead's website. |
city | string | No | City name. |
business_name | string | No | Lead's business or company name. |
additional_info | string | No | Free-form notes / additional info on the lead. |
deal_value | number | No | Estimated deal value. |
source | string | No | Free-form source string (e.g. "facebook-leadgen", "landing-page"). Falls back to the integration's configured source. |
source_type | string | No | One of the predefined source types. See List source types. Falls back to the integration's configured source_type. |
assignee_user_id | number | No | ID of a Superfone team member to assign the lead to. If omitted, the lead is round-robin assigned across the integration's configured assignee_user_ids. |
lead_stage_id | number | No | ID of a lead stage. Falls back to the integration's configured stage. |
lead_group_id | number | No | ID of a lead group. Falls back to the integration's configured group. |
label_ids | number[] | No | IDs of labels to attach. Falls back to the integration's configured labels. |
custom_text_1 … custom_text_5 | string | No | Custom text fields, mapped to your account's custom field labels. |
custom_numeric_1 … custom_numeric_2 | number | No | Custom numeric fields. |
custom_date_1 … custom_date_2 | string | No | Custom date fields, ISO 8601 format. |
Maximum body size: 512 KB.
Address object
| Field | Type | Description |
|---|---|---|
text | string | null | Free-form address text |
additional | string | null | Apartment / unit / additional line |
initials | string | null | Address initials (used for display) |
latitude | number | null | Latitude |
longitude | number | null | Longitude |
If you pass a plain string for address, it's stored as { "text": "<your string>" }.
Field-fallback behavior
For lead_stage_id, lead_group_id, label_ids, source, and source_type: if the field is omitted from the payload, the value configured on the integration setting is used. For assignee_user_id: when omitted, the lead is round-robin assigned across the integration's configured assignee_user_ids pool. This lets you keep payloads minimal — only include a field when you want to override the integration's default.
Try it
Paste your signing secret and integration's webhook UUID below. The playground computes a fresh x-webhook-signature, x-webhook-timestamp, and idempotency-key each time you click Send or Copy as cURL.
Code Examples
The examples below serialize the body once, sign over those exact bytes, and reuse them as the request body. Do not re-serialize between signing and sending.
- Bash (cURL + openssl)
- JavaScript (Node)
- TypeScript
- Python
#!/usr/bin/env bash
set -euo pipefail
WEBHOOK_UUID="8f4e2a31-2b6d-4c3a-9f1e-7a0b6c2d4e5f"
SIGNING_SECRET="your_signing_secret_here"
URL="https://prod-api.superfone.co.in/superfone/v2/webhook/integration/${WEBHOOK_UUID}"
BODY='{"customer_phone":"+918000000001","first_name":"Asha","last_name":"Kumar","email":"asha@example.com","business_name":"Acme Pvt Ltd","city":"Bengaluru","address":"12 MG Road, Bengaluru","deal_value":50000,"source":"landing-page","additional_info":"Asked for a callback after 6 PM."}'
TIMESTAMP=$(date +%s)
IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
SIGNING_STRING="${TIMESTAMP}.${BODY}.${IDEMPOTENCY_KEY}"
SIGNATURE=$(printf "%s" "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex | awk '{print $2}')
curl -X POST "$URL" \
-H "content-type: application/json" \
-H "x-webhook-signature: signature=${SIGNATURE}" \
-H "x-webhook-timestamp: ${TIMESTAMP}" \
-H "idempotency-key: ${IDEMPOTENCY_KEY}" \
--data "$BODY"
const crypto = require("crypto");
const WEBHOOK_UUID = process.env.SF_WEBHOOK_UUID;
const SIGNING_SECRET = process.env.SF_SIGNING_SECRET;
const URL =
"https://prod-api.superfone.co.in/superfone/v2/webhook/integration/" +
WEBHOOK_UUID;
async function pushLead(payload) {
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const idempotencyKey = crypto.randomUUID();
const signingString = `${timestamp}.${body}.${idempotencyKey}`;
const signature = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(signingString)
.digest("hex");
const res = await fetch(URL, {
method: "POST",
headers: {
"content-type": "application/json",
"x-webhook-signature": `signature=${signature}`,
"x-webhook-timestamp": timestamp,
"idempotency-key": idempotencyKey,
},
body,
});
return res.json();
}
pushLead({
customer_phone: "+918000000001",
first_name: "Asha",
last_name: "Kumar",
email: "asha@example.com",
business_name: "Acme Pvt Ltd",
city: "Bengaluru",
address: "12 MG Road, Bengaluru",
deal_value: 50000,
source: "landing-page",
additional_info: "Asked for a callback after 6 PM.",
}).then(console.log);
import crypto from "crypto";
interface WebhookLeadPayload {
customer_phone: string;
first_name?: string;
last_name?: string;
email?: string | string[];
address?:
| string
| {
text?: string | null;
additional?: string | null;
initials?: string | null;
latitude?: number | null;
longitude?: number | null;
};
website?: string;
city?: string;
business_name?: string;
additional_info?: string;
deal_value?: number;
source?: string;
source_type?:
| "CSV_UPLOAD"
| "FACEBOOK_INTEGRATION"
| "PHONE_CONTACT"
| "WHATSAPP_MESSAGE"
| "WHATSAPP_INTEGRATION"
| "PABBLY"
| "OTHERS";
assignee_user_id?: number;
lead_stage_id?: number;
lead_group_id?: number;
label_ids?: number[];
}
interface WebhookResponse {
event_id: string;
status: "accepted" | "duplicate";
request_id: string;
}
async function pushLead(
payload: WebhookLeadPayload
): Promise<WebhookResponse> {
const url =
"https://prod-api.superfone.co.in/superfone/v2/webhook/integration/" +
process.env.SF_WEBHOOK_UUID!;
const signingSecret = process.env.SF_SIGNING_SECRET!;
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const idempotencyKey = crypto.randomUUID();
const signingString = `${timestamp}.${body}.${idempotencyKey}`;
const signature = crypto
.createHmac("sha256", signingSecret)
.update(signingString)
.digest("hex");
const res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
"x-webhook-signature": `signature=${signature}`,
"x-webhook-timestamp": timestamp,
"idempotency-key": idempotencyKey,
},
body,
});
if (!res.ok) {
throw new Error(`Webhook failed: ${res.status} ${await res.text()}`);
}
return (await res.json()) as WebhookResponse;
}
import hashlib
import hmac
import json
import os
import time
import uuid
import requests
WEBHOOK_UUID = os.environ["SF_WEBHOOK_UUID"]
SIGNING_SECRET = os.environ["SF_SIGNING_SECRET"]
URL = (
"https://prod-api.superfone.co.in/superfone/v2/webhook/integration/"
+ WEBHOOK_UUID
)
payload = {
"customer_phone": "+918000000001",
"first_name": "Asha",
"last_name": "Kumar",
"email": "asha@example.com",
"business_name": "Acme Pvt Ltd",
"city": "Bengaluru",
"address": "12 MG Road, Bengaluru",
"deal_value": 50000,
"source": "landing-page",
"additional_info": "Asked for a callback after 6 PM.",
}
body = json.dumps(payload, separators=(",", ":"))
timestamp = str(int(time.time()))
idempotency_key = str(uuid.uuid4())
signing_string = f"{timestamp}.{body}.{idempotency_key}"
signature = hmac.new(
SIGNING_SECRET.encode("utf-8"),
signing_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
response = requests.post(
URL,
data=body,
headers={
"content-type": "application/json",
"x-webhook-signature": f"signature={signature}",
"x-webhook-timestamp": timestamp,
"idempotency-key": idempotency_key,
},
)
print(response.status_code, response.json())
Success Response
Status Code: 202 Accepted
{
"event_id": "ev_01HXYZK7QF2A3B4C5D6E7F8G9H",
"status": "accepted",
"request_id": "req_a1b2c3d4e5f6"
}
202 means Superfone has authenticated and durably queued your payload — the actual lead processing happens asynchronously in a background worker. The response does not indicate whether the lead was newly created vs. updated, or which agent was assigned. Use the dashboard activity log (filtered by event_id) to observe the processing outcome.
Response Fields
| Field | Type | Description |
|---|---|---|
event_id | string | Stable identifier for this delivery. Use this when correlating with the activity log or contacting support. |
status | "accepted" | "duplicate" | accepted for new deliveries. duplicate when the idempotency-key was seen before with the same body — the original event_id is returned. |
request_id | string | Server-side trace ID. Mirrors the x-request-id request header if you sent one, otherwise generated by Superfone. |
Response Headers
| Header | Description |
|---|---|
x-request-id | Same value as request_id in the body. |
event_id for supportEvery delivery is logged in the dashboard activity feed against this event_id. Quote it in any support request about a specific lead.
Error Responses
| Status | Message | When it occurs |
|---|---|---|
400 | Missing required headers: … | One or more of x-webhook-signature, x-webhook-timestamp, idempotency-key is absent. |
400 | Invalid x-webhook-timestamp | x-webhook-timestamp is not a positive integer. |
401 | Invalid signature or webhook | Unified for: unknown webhook_uuid, bad signature, or timestamp outside the ±5 min drift window. The single error message prevents UUID enumeration via timing or content differences. |
403 | Webhook disabled | The webhook exists but is set to DISABLED in the dashboard. Re-enable it or recreate the integration. |
409 | idempotency-key reused with different body | The same idempotency-key was sent earlier with a different body. Reuse only when retrying the same payload. |
413 | Payload too large | Request body exceeds the 512 KB cap. |
429 | Too many requests | Per-IP or per-webhook rate limit hit. Response includes Retry-After: 60. Back off and retry with the same idempotency-key. |
500 | Webhook body could not be verified | Internal error reading the raw body. Retry with a fresh idempotency-key. |
Example error
{
"message": "Missing required headers: x-webhook-signature, idempotency-key"
}
HTTP errors above are returned before the lead is queued — fix the request and resend. Failures during async processing (no active subscription, lead storage full, invalid phone after parsing, etc.) are recorded in the dashboard activity log against the event_id. Check there when a 202 Accepted doesn't seem to have created a lead.
Idempotency and retries
The endpoint is fully idempotent when used correctly:
- Reuse the same
idempotency-keywhen retrying a failed request (network error, 5xx, 429). Superfone caches the key for 24 hours and returnsstatus: "duplicate"with the originalevent_idinstead of processing twice. - Generate a new
idempotency-keyfor every new logical delivery (different lead, different attempt at delivering the same lead after a clean failure on your side, etc.). - Don't reuse the key with a different body — you'll get
409 Conflict.
Recommended retry policy:
- Retry only on
5xxand429. - Use exponential backoff with jitter, starting at 1–2 seconds.
- Honor the
Retry-Afterheader on429. - Cap retries at 5 attempts per
idempotency-key.
Related Endpoints
- Integrations overview — Auth model, rate limits, async delivery semantics
- Customer Management overview — Server-to-server CRM API for richer reads/writes
- Create or Update Lead — Upsert by phone with name-based catalog references (no IDs needed)
- List lead stages, lead groups, labels — Discover IDs to use in
lead_stage_id,lead_group_id,label_ids