Hardening External API Boundaries: Runtime Validation with Zod
Hardening External API Boundaries: Runtime Validation with Zod
Release: v1.0.94 · Supply Chain Security Control SCR-05
Background
NurtureHub connects to a wide range of external services to power its core features:
| Provider | Purpose |
|---|---|
| OpenAI | AI-generated nurture email sequences |
| Resend | Transactional and marketing email delivery |
| Twilio | SMS and voice alerts |
| agentOS | Native CRM integration |
| Reapit / Alto / Street / Loop | Open API CRM sync |
| Meta / Google Ads | Lead source attribution |
| Apify | Web data enrichment |
Every one of these providers owns their own API schema. They can change it — intentionally or not — at any time.
The Problem with Type Assertions Alone
TypeScript is a compile-time tool. When you write:
const response = await fetch(url);
const data = await response.json() as OpenAICompletionResponse;
...TypeScript trusts you. It does not verify that what comes back from the network actually matches OpenAICompletionResponse. At runtime, the types are gone.
This means that if OpenAI were to rename a field, restructure the choices array, or introduce a new required property, the application would receive data it didn't expect — and silently treat missing values as undefined. Depending on where that undefined propagated, the result could range from a broken email sequence to an unhandled exception with an opaque stack trace.
The agentOS adapter used optional chaining (?.) throughout, which helped avoid hard crashes, but it masked the underlying issue rather than surfacing it.
The Fix: Zod Schemas at the API Boundary
Zod is a TypeScript-first schema declaration and validation library. It validates data at runtime — exactly where TypeScript cannot help — and throws descriptive errors when the shape of incoming data does not match expectations.
With v1.0.94, Zod schemas have been added for the most critical external response shapes:
OpenAI Completion Responses
The choices array is central to how NurtureHub generates email sequences. A schema now enforces that choices exists, is an array, and that each entry contains the expected message structure before any downstream processing occurs.
Resend Send Responses
The id field returned by Resend is used to track delivery status and correlate send events. The schema ensures this field is always present as a string — preventing silent tracking failures if Resend's response format changes.
CRM Contact Page Responses (Reapit)
Reepit's paged contact responses feed the bidirectional CRM sync. The schema validates the pagination envelope and contact record structure, so a provider-side schema change (such as the previously undocumented _embedded → data migration risk) is caught immediately at the boundary.
Primary file changed: src/lib/crm/adapters/reapit.ts
What This Changes for Developers
Before
// No runtime check — TypeScript trusts the cast
const completion = data as OpenAICompletionResponse;
const content = completion.choices[0].message.content; // silently undefined if schema drifted
After
// Zod parses and validates at runtime
const completion = OpenAICompletionSchema.parse(data);
const content = completion.choices[0].message.content; // throws ZodError with full detail if invalid
If a provider's response fails validation, a ZodError is thrown with a precise description of which field failed and why. This surfaces in logs and error tracking immediately, with full context — rather than bubbling up as a confusing TypeError: Cannot read properties of undefined several frames deep.
Compatibility
This is a purely additive change. No existing integration logic, data flow, or API contract has been modified. The Zod schemas sit at the inbound data boundary only, and all valid responses that were handled correctly before will continue to pass through without any change in behaviour.
Why This Matters
External API schemas are outside NurtureHub's control. Providers deprecate fields, change response structures, and introduce new required keys — often without notice. Treating the boundary between NurtureHub and the outside world as a trusted, static interface is a supply chain risk.
Runtime validation at that boundary means:
- Schema drift is detected immediately, not discovered when a customer reports a broken email sequence.
- Error messages are precise, pointing directly at the provider and field that changed.
- The rest of the application can be trusted to work with well-shaped data, reducing the need for defensive
?.chaining throughout the codebase.
This change implements supply chain security control SCR-05 as part of NurtureHub's ongoing API reliability programme.