Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

Zero-Disk Webhook Normalization: Stopping ServiceTitan v2 Schema Rejects & Retry Storms in 0.05ms

Last week I wrote about handling HTTP 429 burst lockouts and OAuth token stampedes when piping high-volume lead traffic into ServiceTitan v2. Rate limiting solves the transport bottleneck, but it does nothing for the a

Last week I wrote about handling HTTP 429 burst lockouts and OAuth token stampedes when piping high-volume lead traffic into ServiceTitan v2.

Rate limiting solves the transport bottleneck, but it does nothing for the actual payload errors that quietly kill 6% to 9% of paid Google LSA, Meta, and aggregator leads before dispatch ever sees them.

If you manage webhook infrastructure for multi-branch HVAC, plumbing, or roofing fleets, you have probably watched Zapier or Make bridges choke on three specific edge cases:

  1. Phone Extensions Breaking E.164 Validation: A commercial facility manager types (512) 482-9104 ext. 42 (do not call after 8pm) into a form. Strict E.164 validators reject the string, or worse, strip it down to a malformed 12-digit number that fails customer lookup.
  2. Webhook Retry Storms Creating Duplicate Jobs: When an upstream webhook sender times out, it fires the same payload three times with slightly mutated metadata (retry_count: 2 or a new _delivery_timestamp_ms). Naive payload hashing sees three distinct hashes and books three duplicate dispatches on the board.
  3. PII Storage Liability & Nov 2026 TCPA Revocation Rules: Storing raw customer JSON in a staging database just to debug dropped leads creates a massive compliance headache. At the same time, missing an explicit opt-out signal buried in a notes field ("stop calling this number") exposes the fleet to $500 to $1,500 per call in statutory TCPA penalties.

At Kalloway & Thayer Systems, we engineered our edge ingestion proxy (KT-pipeline-core) to repair these payloads in volatile V8 RAM in ~0.05ms while maintaining a strict Zero-Data-at-Rest posture. Zero customer PII ever touches a disk or SQLite file.

Here is how the normalization, idempotency, and telemetry layers work under the hood.

1. Stripping Call Notes and Isolating Phone Extensions

Before you can validate a North American Numbering Plan (NANP) phone number, you have to deal with the garbage humans type into unvalidated web forms.

In our pipeline, we strip trailing parenthetical call instructions first, isolate the extension so it can be mapped cleanly to ServiceTitan's memo field, and then verify the 10-digit exchange against reserved FCC test numbers (555-01XX):

let phoneBase = rawPhone.replace(/^tel:/i, '').trim();

// Strip trailing call instructions before isolating extensions (e.g., "(do not call after 8pm)")
const noteParenMatch = phoneBase.match(/\s*\((?!\d{3}\))[^)]*[a-z]{3,}[^)]*\)\s*$/i);
if (noteParenMatch && !/\b(ext|extension)\b/i.test(noteParenMatch[0])) {
  phoneBase = phoneBase.slice(0, noteParenMatch.index).trim();
}

let phoneExtension = '';
const extMatch = phoneBase.match(/(?:\s+|[,;(-]\s*)(?:ext\.?|extension|x|#)\s*(\d{1,6})\)?(?:\s*\([^)]*\))?\s*$/i);
if (extMatch) {
  phoneExtension = extMatch[1];
  phoneBase = phoneBase.slice(0, extMatch.index).trim();
}

let digits = phoneBase.replace(/\D/g, '');
if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1);

const isReservedTestExchange = digits.length === 10 && /^55501\d{2}$/.test(digits.slice(3));
const isNanpValid = /^[2-9](?!11)\d{2}[2-9](?!11)\d{2}\d{4}$/.test(digits) && !isReservedTestExchange;
const cleanPhone = isNanpValid ? '+1' + digits : '';

Instead of dropping the webhook or losing the extension, the adapter compiles the output directly into a valid ServiceTitan v2 contact array:

"contacts": [
  { "type": "MobilePhone", "value": "+15124829104", "memo": "ext 42" }
]

We also run a boundary-safe negative lookahead regex against the raw job notes to gate TCPA revocations at the edge (/\b(stop\s+calling|do\s+not\s+call(?!\s*(?:after|before|between|until|prior|during|at\s+work|while)\b)|opt\s*out|unsubscribe|remove\s+me)\b/i). That ensures a customer writing "do not call before 9am" gets routed to dispatch, while "do not call me" gets quarantined before an auto-dialer ever fires.

2. Atomic Idempotency Without Redis or Disk Serialization

To block duplicate dispatches during webhook retry storms without storing customer phone numbers or emails in a database, we compute a composite 64-bit fingerprint in RAM using both DJB2 and FNV-1a (Math.imul).

Crucially, we strip retry suffixes (like evt_9948102_RETRY_1) and ignore transient delivery timestamps when seeding the hash:

const generateFingerprint = (str) => {
  if (!str || !String(str).replace(/\|/g, '').trim()) return null;
  const s = String(str);
  let djb2 = 5381;
  let fnv = 2166136261;
  for (let i = 0; i < s.length; i++) {
    const char = s.charCodeAt(i);
    djb2 = ((djb2 << 5) + djb2) + char;
    fnv ^= char;
    fnv = Math.imul(fnv, 16777619);
  }
  return 'djb2:' + (djb2 >>> 0).toString(16).padStart(8, '0') + '|fnv1a:' + (fnv >>> 0).toString(16).padStart(8, '0');
};

The resulting hash (djb2:7f8a9102|fnv1a:3c4d5e6f) is stored in a null-prototype volatile ledger (Object.create(null)) capped at 2,500 keys with an automatic TTL sweep. If an aggregator double-fires a lead 200ms apart, the second request hits the in-batch or cross-execution lock and gets quarantined to the Dead-Letter Queue (with PII redacted) before it can create a duplicate job in ServiceTitan.

We use the exact same pass to fix XML-to-JSON warehouse purchase order syncs (where single-item line_items objects break array schemas and IEEE-754 float drift ruins Sage Intacct reconcilation).

3. Proving ROI With Zero Customer PII Stored

The biggest pushback from enterprise IT and RevOps teams when adding an edge proxy is observability: "If you don't log the payloads to disk, how do we know what you fixed?"

You don't need to store customer names or phone numbers to prove value. Our runtime allocates ~120 bytes per tenant in volatile RAM to track strict integer counters. At any point, an operator or agency can hit GET /telemetry/:tenant?format=text and pull a plain-text executive receipt:

====================================================================
KALLOWAY & THAYER SYSTEMS LLC - EDGE INGRESS TELEMETRY RECEIPT
====================================================================
Tenant Route:                    ridgeway_southwest_hvac
Zero-Data-at-Rest Posture:       VERIFIED (0 Bytes Customer PII Stored)
--------------------------------------------------------------------
Total Payloads Processed:        1,000 (Avg compute: 0.04ms)
Clean Dispatches Routed:         942
Malformed Payloads Rescued:      184 (~$22,080 ad spend preserved @ $120/lead)
  ├─ E.164 Phones Repaired:      112
  ├─ Phone Extensions Isolated:  48
  ├─ Raw Addresses Structured:   89
  └─ Nested Schemas Flattened:   64
Duplicate Dispatches Blocked:    38 (Atomic DJB2/FNV-1a RAM shield)
HTTP 429 Burst Spikes Paced:     14 (Downstream CRM lockouts prevented)
TCPA / Revocation Flagged:       6 ($3,000-$9,000 statutory exposure gated)
Quarantined to Encrypted DLQ:    14 (0 Silent Drops)
====================================================================

Try the Primitives or Run a Live Payload Test

If you are building custom webhook integrations in TypeScript or Node.js, you can pull our open-source client and normalizer primitives directly from npm:

npm i @kallowaythayer/servicetitan-v2

If you want to see how your current webhook payloads hold up against strict E.164, single-line address splitting, and ServiceTitan v2 schema validation, drop a raw JSON string into the live sandbox on our site or check out the repo.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.