Skip to main content
All Docs
FeaturesMaking Tax DigitalUpdated March 25, 2026

How We Validate External API Responses at Runtime

How We Validate External API Responses at Runtime

Release: v1.0.440 · Control: SCR-05 · Category: API Connection / Supply Chain Security

This platform connects to three external APIs — HMRC, TrueLayer (bank feeds), and AgentOS (property management). Every network response those services return is now validated at runtime using Zod before any application code touches the data.

The Problem With TypeScript Casts Alone

TypeScript's type system is erased at compile time. A pattern like:

const data = JSON.parse(responseBody) as HmrcBusinessDetailsResponse;

…gives you no runtime protection whatsoever. If HMRC adds, removes, or renames a field, the cast succeeds silently and the malformed object propagates through the application. For a tax compliance platform, that means potentially incorrect quarterly figures or failed HMRC submissions — both of which have real consequences for users.

The Solution: Zod Schemas at API Boundaries

Every response from an external API is now parsed through a Zod schema immediately after JSON.parse(). If the shape of the response does not match the schema, a structured error is thrown before the data reaches any business logic.

Pattern

import { z } from 'zod';

const HmrcTokenResponseSchema = z.object({
  access_token: z.string(),
  refresh_token: z.string(),
  expires_in: z.number(),
  token_type: z.string(),
});

type HmrcTokenResponse = z.infer<typeof HmrcTokenResponseSchema>;

// Inside the client:
const raw = JSON.parse(responseBody);
const data = HmrcTokenResponseSchema.parse(raw); // throws on mismatch

Using z.infer<typeof Schema> means the TypeScript type and the runtime validator are always derived from the same source of truth — there is no opportunity for the type and the schema to drift apart.

Schemas Introduced in v1.0.440

HMRC (src/lib/hmrc/client.ts)

SchemaPurpose
HmrcTokenResponseSchemaOAuth token exchange and refresh responses
HmrcBusinessDetailsResponseSchemalistOfBusinesses endpoint — identifies business IDs used in submissions
HmrcObligationsResponseSchemaQuarterly and annual obligation periods

TrueLayer

SchemaPurpose
TrueLayerTokensSchemaOAuth token responses from TrueLayer
TrueLayerAccountSchemaBank account metadata
TrueLayerTransactionSchemaIndividual transaction records imported for categorisation
TrueLayerBalanceSchemaAccount balance data

AgentOS

All AgentOS response payloads are validated against defined Zod schemas before property or tenancy data is used by the application.

Error Behaviour

When a schema validation fails, the client throws a structured error containing:

  • The name of the schema that failed
  • The Zod validation error details (which fields were missing, wrong type, etc.)
  • The originating API (HMRC / TrueLayer / AgentOS)

This means failures are immediately visible in logs and error monitoring — there is no silent processing of malformed data.

// Example structured error shape
{
  source: 'hmrc',
  schema: 'HmrcBusinessDetailsResponse',
  issues: [
    { path: ['listOfBusinesses', 0, 'businessId'], message: 'Required' }
  ]
}

Why HMRC Responses Are the Highest Priority

The listOfBusinesses response from HMRC contains the business IDs used in every subsequent quarterly submission and final declaration. If this response were malformed and processed without validation, downstream submissions could reference incorrect or missing IDs — resulting in failed or incorrect MTD ITSA submissions. Zod validation here acts as a hard stop before any submission data is written.

Using safeParse for Non-Critical Paths

For paths where a validation failure should degrade gracefully rather than throw, use safeParse:

const result = TrueLayerTransactionSchema.safeParse(raw);
if (!result.success) {
  logger.warn('Unexpected transaction shape', { issues: result.error.issues });
  return null; // skip this transaction rather than crash
}
const transaction = result.data;

Summary

Before v1.0.440From v1.0.440
JSON.parse() → TypeScript castJSON.parse()schema.parse() → typed value
No runtime shape checkFull field-level validation at the API boundary
Malformed data propagates silentlyStructured error thrown immediately on mismatch
Types can drift from actual API responsesTypes derived from schemas — always in sync