Migrate v1 → v2
The v1 integration webhook is deprecated. It continues to accept requests until 30-June-2026; after that date v1 endpoints will stop processing leads and return errors. Migrate to v2 before then.
v2 adds:
- HMAC-SHA256 request signing — the
webhook_uuidalone is no longer a credential. - Idempotency keys for safe retries with 24-hour dedupe.
- Async
202 Acceptedprocessing with a stableevent_idfor correlation.
After this date, requests to the v1 URL will fail. Dashboard configuration (assignee pool, lead stage, lead group, labels, task config) carries over automatically — the migration is a client-side change to URL, headers, and a couple of body field names.
Identify your version
The URL path tells you which version your integration is on.
| Version | URL pattern | Distinguishing feature |
|---|---|---|
| v1 | https://prod-api.superfone.co.in/superfone/webhook/integration/<uuid> | No version segment in the path |
| v2 | https://prod-api.superfone.co.in/superfone/v2/webhook/integration/<uuid> | Path contains /v2/ |
If the URL you currently POST to does not contain /v2/, you are on v1 and must migrate.
What changes
| Concern | v1 | v2 |
|---|---|---|
| URL | /webhook/integration/:webhook_uuid | /v2/webhook/integration/:webhook_uuid |
| Auth | UUID alone acted as the credential | HMAC-SHA256 signature over <timestamp>.<body>.<idempotency-key> |
| Required headers | content-type only | content-type, x-webhook-signature, x-webhook-timestamp, idempotency-key |
| Response | 200 OK with { "success": true, "lead_id": <number> } | 202 Accepted with { event_id, status, request_id } — async |
| Idempotency | None | Required idempotency-key; 24-hour dedupe window |
| Body field — phone | phone | customer_phone |
Other body fields (first_name, last_name, business_name, address, city, source, custom fields, etc.) are unchanged. See Receive Lead Webhook for the complete v2 body schema.
Migration steps
Work through the phases in order. Steps 1–4 are zero-risk code prep you can do on a feature branch; step 5 is the cutover; steps 6–7 verify and clean up.
Phase 1 — Prep (no traffic changes)
- Get your v2 webhook URL and signing secret. Open the integration in the Superfone dashboard and copy both values. Treat the signing secret like a password: store it in a secret manager, never commit it to a repo, never ship it to a browser.
- Update the URL in your caller from
/webhook/integration/<uuid>to/v2/webhook/integration/<uuid>. The<uuid>itself does not change. - Rename body fields:
phone→customer_phone- Leave every other field as-is —
email,first_name,last_name,business_name,address,city,source, custom fields, etc. are unchanged.
Phase 2 — Add signing and headers
- Generate a per-request
idempotency-key(UUIDv4 is a good default) and a Unix-secondsx-webhook-timestamp. Keep both — you'll need them for the signature and the headers. - Serialize the body once to a string of bytes. Don't pretty-print, sort keys, or re-serialize after this point — the signature is computed over the exact bytes you send.
- Compute the signature as
HMAC-SHA256(<timestamp>.<body>.<idempotency-key>)using your signing secret as the HMAC key, then hex-encode (lowercase). See Signing for language-specific code. - Send all four required headers alongside the body:
content-type: application/jsonx-webhook-signature: signature=<hex-digest>x-webhook-timestamp: <unix-seconds>idempotency-key: <uuid>
Phase 3 — Validate against v2 in a non-production environment
- Send a known-good payload to the v2 URL from a staging or test integration. Confirm the response is
202 Acceptedwith a non-emptyevent_id. - Open the dashboard activity log for that integration and confirm the event appears with the same
event_id, and that a lead was created or updated as expected. - Exercise error paths: send a bad signature (expect
401), send a stale timestamp (expect401), replay the sameidempotency-key(expectstatus: "duplicate"with the originalevent_id). This catches signing bugs before they touch production.
Phase 4 — Cut over production traffic
- Switch the production caller to v2. A flag-gated cutover (e.g. 1% → 10% → 100% by environment or tenant) is the safest path; an instant flip is fine if your test coverage from Phase 3 is solid.
- Don't dual-write. Posting the same lead to both v1 and v2 will create duplicate processing on each side — they don't share idempotency. Switch each caller from v1 to v2 in a single change.
- Adjust your retry policy to retry only on
5xxand429, with exponential backoff plus jitter, reusing the sameidempotency-keyacross retries of the same delivery. See Idempotency and retries. - Stop reading
lead_idfrom the response. v2 returns202 Acceptedas soon as the payload is queued; thelead_idis no longer in the response body. If you need the lead ID, correlate byevent_idin the dashboard activity log, or look up the lead by phone via the Customer Management API.
Phase 5 — Verify and clean up
- Watch the activity log and your error metrics for 24–48 hours after cutover. Healthy v2 traffic looks like
202responses, matching events in the log, no401/409clusters. - Decommission the v1 caller path — remove the old URL, the old payload shape, and any code that read
success/lead_idfrom the v1 response. - Rotate the signing secret from the dashboard if it was pasted anywhere insecure during development (logs, screenshots, test fixtures).
Before / after request
v1 — unsigned POST, sync 200:
curl -X POST "https://prod-api.superfone.co.in/superfone/webhook/integration/8f4e2a31-2b6d-4c3a-9f1e-7a0b6c2d4e5f" \
-H "content-type: application/json" \
-d '{"phone":"+918000000001","first_name":"Asha","email":"asha@example.com"}'
# 200 OK
# {"success": true, "lead_id": 1234567}
v2 — HMAC-signed POST, async 202:
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","email":"asha@example.com"}'
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"
# 202 Accepted
# {"event_id":"ev_01HXYZK7QF2A3B4C5D6E7F8G9H","status":"accepted","request_id":"req_a1b2c3d4e5f6"}
Language-specific signed examples (Node, TypeScript, Python) are in Receive Lead Webhook → Code Examples.
How to confirm the migration worked
- Send a test payload to the v2 URL and confirm the response is
202 Acceptedwith a non-emptyevent_id. - Open the dashboard activity log and verify the event appears against your integration with the matching
event_id. - Check that the lead was created or updated as expected (round-robin assignment, configured stage/group/labels applied).
- After a few days of clean v2 traffic, retire the v1 caller path.
Common migration pitfalls
- Re-serializing the body between signing and sending. The signature is computed over the exact body bytes sent on the wire. Serialize once, sign those bytes, send those bytes.
- Clock drift.
x-webhook-timestampmust be within ±5 minutes of Superfone's clock. NTP-sync the box that builds the request. - Reusing
idempotency-keywith a different body. This returns409 Conflict. Generate a new key per logical delivery; reuse the same key only when retrying the same payload. - Treating
202as a processing failure. v2 is async by design —202means "queued", not "lead created". Check the activity log for the outcome.
Related
- Receive Lead Webhook — full v2 request/response reference and signed code samples
- Integrations overview — auth model, rate limits, async semantics, idempotency