@sdltcheck/sdlt-client
v1.3.0
Published
Official SDK for the SDLTCheck SDLT1/SDLT2/SDLT3/SDLT4 HMRC XML Builder API. Drives a conversational SDLT questionnaire and produces HMRC-compliant XML.
Maintainers
Readme
@sdltcheck/sdlt-client
Official Node.js / TypeScript SDK for the SDLTCheck SDLT1 / SDLT2 / SDLT3 / SDLT4 HMRC XML Builder API.
Build and submit Stamp Duty Land Tax returns from your own application without having to internalise HMRC's question codes, conditional logic, supplementary-form rules, or XML schema. Drive a conversational questionnaire, get HMRC-compliant XML out the other end.
API access
This SDK is part of a paid product. To request an API key, sandbox credentials, or to discuss volume pricing:
- Email: [email protected]
- Web: https://sdltcheck.co.uk
- Support: [email protected]
Why use it?
| Without SDLTCheck | With SDLTCheck |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Hand-build a 70+ question conditional questionnaire | Hit one endpoint per answer; the API tells you what to ask next |
| Track which conditions trigger SDLT2 / SDLT3 / SDLT4 supplementary forms | The API tells you in response.supplementary and emits the right XML |
| Validate UK postcodes, NINOs, UTRs, VAT numbers, UPRNs, dates, cross-fields | Built-in. Validation errors come back as structured errors[] |
| Build HMRC-namespaced XML envelopes by hand | XML returned ready-to-submit with <SDLTReturn xmlns="…/SDLT/2/0"> and Header |
| Keep the SDLT form structure in sync with HMRC changes | Centrally maintained. Upgrade the SDK and you're current |
Install
npm install @sdltcheck/sdlt-client
# or
pnpm add @sdltcheck/sdlt-client
# or
yarn add @sdltcheck/sdlt-clientRequires Node.js 18+ (uses native fetch). For older runtimes, pass a polyfill via the fetchImpl option.
Quick start
import { SdltCheckClient } from '@sdltcheck/sdlt-client';
const client = new SdltCheckClient({
apiKey: process.env.SDLT_API_KEY!,
});
// 1. Drive the questionnaire one answer at a time
const result = await client.nextQuestion({
answers: { effectiveDate: '2026-06-01' },
});
if (result.type === 'question') {
console.log('Next question:', result.question.question);
console.log('Progress:', result.progress.percent, '%');
} else {
console.log('Done. XML reference:', result.reference);
console.log(result.xml);
}Conversational flow — the core idea
The SDLTCheck API is stateless. You hold the answers, the API tells you what's next.
┌─────────────┐ POST /api/v1/next-question ┌──────────────┐
│ Your App │ ────────────────────────────► │ SDLTCheck │
│ │ ◄──────────────────────────── │ API │
│ answers │ { type: "question", ... } └──────────────┘
│ { ... } │ or
└─────────────┘ { type: "xml", xml: "..." }Every call:
- You POST the full running answer set.
- The API evaluates conditional
showWhenrules, finds the first incomplete required question, and returns it. Or, if everything's answered, returns the assembled HMRC XML.
You decide whether to ask the user the question, derive the answer automatically, skip optional ones, or pre-seed the entire set and call once.
Usage patterns
1. Step-by-step (interactive UI)
let answers = {};
while (true) {
const r = await client.nextQuestion({ answers });
if (r.type === 'xml') return r; // Done.
const ans = await askUser(r.question); // your UI prompt
answers = { ...answers, [r.question.id]: ans };
}2. One-shot with runToCompletion
const result = await client.runToCompletion({
initialAnswers: { effectiveDate: '2026-06-01' },
onQuestion: async (question, soFar, meta) => {
// Decide an answer however you like — UI prompt, derived from your DB,
// looked up from a CRM, etc.
return await deriveAnswerFor(question, soFar);
},
});
console.log(result.xml);3. Direct generation when answers are complete
If your application already has every answer (e.g. imported from a conveyancing CRM), skip the conversational loop entirely:
const built = await client.generateXml({
answers: completeAnswerSet,
reference: 'CASE-2026-0815',
strict: true, // 422 if validation errors are present
});
await submitToHmrc(built.xml);4. Show "answers so far" review screens
const review = await client.answeredQuestions({ answers });
for (const a of review.answered) {
console.log(`${a.sectionTitle} → ${a.question}: ${a.displayAnswer}`);
}
console.log('Status:', review.validationStatus);
console.log('SDLT2 needed?', review.supplementary.sdlt2);MappedAnswer includes:
displayAnswer— formatted for humans ("15 March 2026","£500,000.00", the option label rather than its code)optionLabel— the human label of selected option codeschildren— for list-typed answers, one fully mapped sub-form per itemvalidationStatus—'valid' | 'valid-with-warnings' | 'invalid'
Supplementary forms (SDLT2 / SDLT3 / SDLT4)
You don't decide which supplementary forms apply — the API does, based on the answers. Every response carries:
supplementary: {
sdlt2: boolean; // true when vendors > 2 OR purchasers > 2
sdlt3: boolean; // true when numberOfProperties > 1
sdlt4: boolean; // true for company purchaser, lease, business sale,
// contingent consideration, HMRC ruling, instalment, etc.
}XML responses also include parts.sdlt2Count / parts.sdlt3Count so you can
report the supplementary-form load to your end users.
HMRC identity (multi-tenant)
If you operate the API on behalf of multiple firms, override sender / vendor identity per request:
await client.nextQuestion({
answers,
senderId: 'TENANT-42-SENDER-ID',
vendorId: 'TENANT-42-VENDOR-ID',
agentName: 'Acme Conveyancing LLP', // Q62 fallback
});Or set them once on the client and they apply to every request:
const client = new SdltCheckClient({
apiKey: process.env.SDLT_API_KEY!,
defaultIdentity: {
senderId: 'OUR-SENDER-ID',
vendorId: 'OUR-VENDOR-ID',
},
});Per-request values always win over defaultIdentity.
Error handling
The SDK throws typed errors, all extending SdltCheckError:
import {
SdltCheckClient,
SdltCheckAuthError,
SdltCheckValidationError,
SdltCheckNetworkError,
SdltCheckServerError,
} from '@sdltcheck/sdlt-client';
try {
const r = await client.generateXml({ answers, strict: true });
} catch (err) {
if (err instanceof SdltCheckAuthError) {
// 401 — bad / missing key. Renew via [email protected]
} else if (err instanceof SdltCheckValidationError) {
// 400 / 422 — `err.details` contains the structured issues
console.error(err.details);
} else if (err instanceof SdltCheckServerError) {
// 5xx after retries — the SDK already retried with exponential backoff
} else if (err instanceof SdltCheckNetworkError) {
// DNS / timeout / connection reset
}
}SdltCheckValidationError.details mirrors the API's structured error response
— for /generate-xml (strict: true) it contains both errors[] and
warnings[] arrays you can render directly.
Configuration reference
new SdltCheckClient({
apiKey: string, // required
baseUrl?: string, // default: https://sdlt1.sdltcheck.co.uk
timeoutMs?: number, // default: 30_000
maxRetries?: number, // default: 2 (429 / 5xx / network)
retryBaseMs?: number, // default: 500ms (exponential)
fetchImpl?: typeof fetch, // for older runtimes
defaultHeaders?: Record<string, string>, // tracing, request-id, etc.
defaultIdentity?: {
senderId?: string,
vendorId?: string,
agentName?: string,
},
});API methods
| Method | Maps to | Returns |
| ----------------------------------- | ---------------------------------- | ------------------------------------------ |
| nextQuestion(req) | POST /api/v1/next-question | QuestionResponse \| XmlResponse |
| nextQuestionExpectingXml(req) | (same, asserts XML) | XmlResponse |
| nextQuestionExpectingQuestion(req)| (same, asserts question) | QuestionResponse |
| answeredQuestions(req) | POST /api/v1/answered-questions | AnsweredQuestionsResponse |
| generateXml(req) | POST /api/v1/generate-xml | GenerateXmlResponse |
| runToCompletion({...}) | (driver loop) | XmlResponse |
| health() | GET /health | health payload |
Every type used by these methods is exported from the package root for re-use in your application.
TypeScript
The package is shipped with first-class TypeScript declarations:
Question— the question schema, includingoptions[],validation,listSettingAddress,ListItem— answer shapesMappedAnswer— review-screen entrySupplementaryNeeds— which forms applyXmlResponse/QuestionResponse— discriminated union viatype
Compliance & guarantees
- HMRC-compliant XML —
xmlns="http://www.govtalk.gov.uk/taxation/SDLT/2/0", field length limits enforced, postcodes / NINOs normalised, monetary fields formatted in whole pounds. - Cross-field validation — contract date ≤ effective date, lease end > start, linked-transaction total > 0, non-resident surcharge consistency, paid vs due sanity.
- Versioned — semver. Breaking changes go in major releases; the API
itself is versioned at
/api/v1/…so SDK upgrades within a major are drop-in.
For the GovTalk envelope wrapping required by HMRC's transaction engine
(<GovTalkMessage> etc.), see the SDLTCheck submission guide — request access
via [email protected].
Examples
The package source includes runnable examples under examples/:
examples/basic-flow.ts— a singlenextQuestioncallexamples/full-submission.ts—runToCompletionwith a pre-seeded answer set
Run them with npm run example:basic / npm run example:complete.
Support
- Documentation: https://sdltcheck.co.uk/docs
- Issues / questions: [email protected]
- Sales / API access: [email protected]
© SDLTCheck — All rights reserved.
