npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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:


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-client

Requires 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:

  1. You POST the full running answer set.
  2. The API evaluates conditional showWhen rules, 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 codes
  • children — for list-typed answers, one fully mapped sub-form per item
  • validationStatus'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, including options[], validation, listSetting
  • Address, ListItem — answer shapes
  • MappedAnswer — review-screen entry
  • SupplementaryNeeds — which forms apply
  • XmlResponse / QuestionResponse — discriminated union via type

Compliance & guarantees

  • HMRC-compliant XMLxmlns="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 single nextQuestion call
  • examples/full-submission.tsrunToCompletion with a pre-seeded answer set

Run them with npm run example:basic / npm run example:complete.


Support


© SDLTCheck — All rights reserved.