Skip to main content

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.

Server-to-server recommended

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

ParameterTypeRequiredDescription
webhook_uuidstringYesThe unique identifier issued when you created the integration in the dashboard.

Required Headers

HeaderDescription
content-typeMust be application/json.
x-webhook-signaturesignature=<hex> — HMAC-SHA256 of the signing string. See Signing.
x-webhook-timestampUnix timestamp in seconds when the request was created. Must be within ±5 minutes of Superfone's clock.
idempotency-keyA unique string per delivery (UUIDv4 recommended). See Idempotency.
x-request-idOptional. 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.timingSafeEqual for verification on its side; you should do the same when verifying signatures elsewhere.

Request Body

FieldTypeRequiredDescription
customer_phonestringYesLead's phone number (E.164 preferred, e.g. +918000000001). Used to match an existing lead or create a new one.
first_namestringNoLead's first name. Existing leads keep their stored first/last name if the incoming first_name looks like a phone number.
last_namestringNoLead's last name.
emailstring | string[]NoOne or more email addresses. A single string is accepted and converted to a one-element array.
addressstring | objectNoAddress. A plain string is stored as { "text": "..." }; an object is stored as-is. See Address object.
websitestringNoLead's website.
citystringNoCity name.
business_namestringNoLead's business or company name.
additional_infostringNoFree-form notes / additional info on the lead.
deal_valuenumberNoEstimated deal value.
sourcestringNoFree-form source string (e.g. "facebook-leadgen", "landing-page"). Falls back to the integration's configured source.
source_typestringNoOne of the predefined source types. See List source types. Falls back to the integration's configured source_type.
assignee_user_idnumberNoID 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_idnumberNoID of a lead stage. Falls back to the integration's configured stage.
lead_group_idnumberNoID of a lead group. Falls back to the integration's configured group.
label_idsnumber[]NoIDs of labels to attach. Falls back to the integration's configured labels.
custom_text_1custom_text_5stringNoCustom text fields, mapped to your account's custom field labels.
custom_numeric_1custom_numeric_2numberNoCustom numeric fields.
custom_date_1custom_date_2stringNoCustom date fields, ISO 8601 format.

Maximum body size: 512 KB.

Address object

FieldTypeDescription
textstring | nullFree-form address text
additionalstring | nullApartment / unit / additional line
initialsstring | nullAddress initials (used for display)
latitudenumber | nullLatitude
longitudenumber | nullLongitude

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.

Loading playground…

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.

#!/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"

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

FieldTypeDescription
event_idstringStable 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_idstringServer-side trace ID. Mirrors the x-request-id request header if you sent one, otherwise generated by Superfone.

Response Headers

HeaderDescription
x-request-idSame value as request_id in the body.
Use event_id for support

Every delivery is logged in the dashboard activity feed against this event_id. Quote it in any support request about a specific lead.

Error Responses

StatusMessageWhen it occurs
400Missing required headers: …One or more of x-webhook-signature, x-webhook-timestamp, idempotency-key is absent.
400Invalid x-webhook-timestampx-webhook-timestamp is not a positive integer.
401Invalid signature or webhookUnified 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.
403Webhook disabledThe webhook exists but is set to DISABLED in the dashboard. Re-enable it or recreate the integration.
409idempotency-key reused with different bodyThe same idempotency-key was sent earlier with a different body. Reuse only when retrying the same payload.
413Payload too largeRequest body exceeds the 512 KB cap.
429Too many requestsPer-IP or per-webhook rate limit hit. Response includes Retry-After: 60. Back off and retry with the same idempotency-key.
500Webhook body could not be verifiedInternal error reading the raw body. Retry with a fresh idempotency-key.

Example error

{
"message": "Missing required headers: x-webhook-signature, idempotency-key"
}
Async failures are logged

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-key when retrying a failed request (network error, 5xx, 429). Superfone caches the key for 24 hours and returns status: "duplicate" with the original event_id instead of processing twice.
  • Generate a new idempotency-key for 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:

  1. Retry only on 5xx and 429.
  2. Use exponential backoff with jitter, starting at 1–2 seconds.
  3. Honor the Retry-After header on 429.
  4. Cap retries at 5 attempts per idempotency-key.