<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Solomon Amos</title>
    <description>The latest articles on DEV Community by Solomon Amos (@taptax).</description>
    <link>https://dev.to/taptax</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4008517%2F6c40c43b-b152-411e-885c-205a24f8eb6e.png</url>
      <title>DEV Community: Solomon Amos</title>
      <link>https://dev.to/taptax</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taptax"/>
    <language>en</language>
    <item>
      <title>Decoding UK Tax Codes: What the Gap Between 1257L and 1283L Actually Costs You</title>
      <dc:creator>Solomon Amos</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:35:04 +0000</pubDate>
      <link>https://dev.to/taptax/decoding-uk-tax-codes-what-the-gap-between-1257l-and-1283l-actually-costs-you-1bhh</link>
      <guid>https://dev.to/taptax/decoding-uk-tax-codes-what-the-gap-between-1257l-and-1283l-actually-costs-you-1bhh</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on TapTax: &lt;a href="https://taptax.co.uk/blog/1257l-vs-1283l-tax-code-what-the-gap-costs-you" rel="noopener noreferrer"&gt;https://taptax.co.uk/blog/1257l-vs-1283l-tax-code-what-the-gap-costs-you&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have ever glanced at a UK payslip and wondered what that cryptic alphanumeric string means, you are not alone. Tax codes like &lt;code&gt;1257L&lt;/code&gt; and &lt;code&gt;1283L&lt;/code&gt; look almost identical, but the difference between them represents real money moving in or out of your pocket every month. As someone who builds or ships software, you probably appreciate that small off-by-one errors compound over time. The same logic applies here.&lt;/p&gt;

&lt;h2&gt;
  
  
  How UK Tax Codes Actually Work
&lt;/h2&gt;

&lt;p&gt;UK income tax codes follow a consistent structure. The numeric portion, divided by ten, gives you your tax-free Personal Allowance for the year. The letter suffix indicates which set of rules applies. &lt;code&gt;L&lt;/code&gt; is the most common suffix, meaning you are on the standard allowance with no special conditions.&lt;/p&gt;

&lt;p&gt;So for the 2025/26 tax year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;1257L&lt;/code&gt; = £12,570 tax-free allowance (the default for most employees)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1283L&lt;/code&gt; = £12,830 tax-free allowance (£260 above the default)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every 10-unit increment in the number equals £100 of additional tax-free income. The 26-unit gap between these two codes therefore represents exactly £260 of allowance. For a basic-rate taxpayer at 20%, that translates to &lt;strong&gt;£52 saved per year&lt;/strong&gt;, or just over &lt;strong&gt;£4 per month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That sounds trivial in isolation. But the mechanism that produces a code like &lt;code&gt;1283L&lt;/code&gt; is worth understanding, because if you are missing it when you should have it, the gap compounds across years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Causes HMRC to Issue 1283L
&lt;/h2&gt;

&lt;p&gt;HMRC does not issue elevated codes at random. When your code exceeds the standard &lt;code&gt;1257L&lt;/code&gt;, it means HMRC has applied a positive adjustment to your Personal Allowance. These adjustments represent reliefs and deductions that have been folded into your PAYE calculation rather than handled through Self Assessment.&lt;/p&gt;

&lt;p&gt;Common reasons your code might legitimately sit above &lt;code&gt;1257L&lt;/code&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Professional subscriptions and union fees
&lt;/h3&gt;

&lt;p&gt;Annual membership fees for bodies on HMRC's approved list (covering hundreds of professional associations) qualify for tax relief. Rather than requiring a separate return, HMRC often raises your code by the equivalent amount. A £260 subscription produces exactly a &lt;code&gt;1283L&lt;/code&gt; code from a &lt;code&gt;1257L&lt;/code&gt; baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flat rate job expenses
&lt;/h3&gt;

&lt;p&gt;HMRC maintains occupation-specific flat rate deductions for roles where employees routinely incur tool, uniform, or equipment costs. Some examples for 2025/26:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Occupation&lt;/th&gt;
&lt;th&gt;Annual flat rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Carpenters and joiners&lt;/td&gt;
&lt;td&gt;£140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General construction / civil engineering&lt;/td&gt;
&lt;td&gt;£140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineers (all types)&lt;/td&gt;
&lt;td&gt;£120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heating and ventilation engineers&lt;/td&gt;
&lt;td&gt;£120&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When claimed, these amounts are added directly to your Personal Allowance, which is precisely how codes above &lt;code&gt;1257L&lt;/code&gt; get generated for tradespeople and technical workers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working from home allowance
&lt;/h3&gt;

&lt;p&gt;Since 2020, HMRC has permitted employees required to work from home to claim £6 per week (£312 per year) via their tax code. If you claimed this and it was applied to your code, your allowance rises accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pension relief for higher-rate taxpayers
&lt;/h3&gt;

&lt;p&gt;Contributions to a personal pension (for example, a SIPP) have basic-rate relief claimed automatically by the provider. But higher-rate relief on the same contributions is not automatic. HMRC typically delivers it via a code adjustment. If you are a higher-rate taxpayer contributing to a personal pension and your code is still &lt;code&gt;1257L&lt;/code&gt;, you may be missing the additional 20% relief entirely. On a £10,000 annual contribution, that omission costs £2,000 per year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prior-year credit adjustments
&lt;/h3&gt;

&lt;p&gt;If HMRC has determined you were overtaxed in a prior period and is spreading a credit back into your code over time, the number will sit above the standard baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Risk When You Have 1283L Without Knowing Why
&lt;/h2&gt;

&lt;p&gt;Receiving a code higher than &lt;code&gt;1257L&lt;/code&gt; is not automatically a win. If the adjustment was applied in error, or if the underlying claim is no longer valid, you are accumulating an underpayment. HMRC will recover it, typically by reducing your code in a subsequent year.&lt;/p&gt;

&lt;p&gt;A common example: you claimed the work-from-home allowance during the pandemic but have since returned to an office full-time. If HMRC continues to apply that relief, you are building a liability. The system will catch up eventually.&lt;/p&gt;

&lt;p&gt;If your code is &lt;code&gt;1283L&lt;/code&gt; and you cannot identify what the extra £260 represents, check your breakdown via the HMRC Personal Tax Account online or call the income tax helpline on &lt;strong&gt;0300 200 3300&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Are Stuck on 1257L and Should Have More
&lt;/h2&gt;

&lt;p&gt;This is the larger problem in aggregate. Millions of UK employees sit on exactly &lt;code&gt;1257L&lt;/code&gt; because it is the default, and neither payroll departments nor HMRC proactively surfaces unclaimed reliefs. The categories most likely to be missing a higher code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Construction and trade workers&lt;/strong&gt; who buy their own tools or maintain specialist equipment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare, dental, or veterinary staff&lt;/strong&gt; paying professional registration fees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees who worked from home&lt;/strong&gt; since 2020 and never submitted a claim&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher-rate taxpayers&lt;/strong&gt; contributing to a personal pension outside of a workplace scheme&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone paying professional subscriptions&lt;/strong&gt; to a body on HMRC's approved list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these apply and your code is still &lt;code&gt;1257L&lt;/code&gt;, you may have been overpaying for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Far Back Can You Claim?
&lt;/h2&gt;

&lt;p&gt;HMRC allows overpaid income tax to be reclaimed for up to &lt;strong&gt;four tax years&lt;/strong&gt;. In the 2025/26 tax year, that window runs back to 2021/22. Every year you delay is a year closer to that deadline.&lt;/p&gt;

&lt;p&gt;For flat rate expenses and professional subscriptions, claims can be submitted through the HMRC Personal Tax Account, by phone, or via a P87 form. Once approved, HMRC will typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adjust your code for the current year going forward, and&lt;/li&gt;
&lt;li&gt;Issue a refund (cheque or bank transfer) for prior years separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your situation involves multiple income sources, pension contributions, or several overlapping adjustments, filing a Self Assessment return consolidates everything in one place.&lt;/p&gt;

&lt;p&gt;For reference: on a £50,000 salary, missing a legitimate £140 flat rate deduction since 2021/22 adds up to £560 of unclaimed allowance, worth £112 at the basic rate or £224 at the higher rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Three-Step Audit
&lt;/h2&gt;

&lt;p&gt;If you have read this far and are not certain your code is correct, uncertainty is a signal worth acting on. UK PAYE does not automatically find money you are owed and return it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pull your current tax code&lt;/strong&gt; from your most recent payslip or P60. Note the full code including the suffix letter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log into your HMRC Personal Tax Account&lt;/strong&gt; and view the breakdown of what components make up your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-reference against your actual circumstances.&lt;/strong&gt; Do you pay professional subscriptions? Buy your own tools? Work from home? Contribute to a personal pension at a higher rate?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If anything is absent from the HMRC breakdown that should be there, you have the right to request a correction and reclaim overpaid tax going back up to four years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wider Point
&lt;/h2&gt;

&lt;p&gt;The gap between &lt;code&gt;1257L&lt;/code&gt; and &lt;code&gt;1283L&lt;/code&gt; is £52 a year. That is not the interesting number. The interesting number is what sits unclaimed across four years for someone who qualifies for a legitimate adjustment and has never asked for it - potentially several hundred pounds sitting in HMRC's account with your name on it.&lt;/p&gt;

&lt;p&gt;The UK tax system defaults to collecting what it assumes you owe. Recovering what you are entitled to requires you to initiate the process.&lt;/p&gt;




&lt;p&gt;TapTax is an MTD-compatible app for UK sole traders, currently going through HMRC's recognition process. You can try it at &lt;a href="https://taptax.co.uk" rel="noopener noreferrer"&gt;https://taptax.co.uk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this article was drafted with AI assistance and edited by the TapTax team.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mtd</category>
      <category>tax</category>
      <category>fintech</category>
      <category>api</category>
    </item>
    <item>
      <title>5 things that surprised me building on HMRC's Making Tax Digital API</title>
      <dc:creator>Solomon Amos</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:53:06 +0000</pubDate>
      <link>https://dev.to/taptax/5-things-that-surprised-me-building-on-hmrcs-making-tax-digital-api-4eja</link>
      <guid>https://dev.to/taptax/5-things-that-surprised-me-building-on-hmrcs-making-tax-digital-api-4eja</guid>
      <description>&lt;p&gt;I spent the last while building the HMRC integration for &lt;a href="https://taptax.co.uk/making-tax-digital?utm_source=devto&amp;amp;utm_medium=editorial&amp;amp;utm_campaign=ebas_devto_mtdapi" rel="noopener noreferrer"&gt;TapTax&lt;/a&gt;, a Making Tax Digital (MTD) app for UK sole traders. MTD is the UK government's programme that pushes tax filing out of paper and spreadsheets and into software talking directly to HMRC's APIs.&lt;/p&gt;

&lt;p&gt;I have integrated with a fair few third-party APIs. Stripe, Plaid-style banking, the usual. HMRC is its own animal. Some of it is genuinely well designed, some of it caught me completely off guard, and a couple of things cost me a full day each before the penny dropped.&lt;/p&gt;

&lt;p&gt;So here are the five things that surprised me most. Each one is the surprise, then the fix, with a short snippet from our actual TypeScript backend. Not tax advice, just engineering notes from someone who has now stepped on the rakes so you do not have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The API version lives in the Accept header, and getting it wrong is a 406
&lt;/h2&gt;

&lt;p&gt;Most APIs version in the URL: &lt;code&gt;/v2/thing&lt;/code&gt;. HMRC versions through content negotiation. You ask for a version in the &lt;code&gt;Accept&lt;/code&gt; header, like &lt;code&gt;application/vnd.hmrc.5.0+json&lt;/code&gt;, and if you ask for a version that endpoint does not serve, you get a &lt;code&gt;406 Not Acceptable&lt;/code&gt;. No helpful "did you mean v3" message. Just 406.&lt;/p&gt;

&lt;p&gt;The part that bit me: different endpoints are on completely different versions at the same time. Obligations is on v3.0, the self-employment cumulative summary is on v5.0, calculations are on v8.0, ITSA status is on v2.0. There is no single "current" version to pin.&lt;/p&gt;

&lt;p&gt;The fix was to make the version a required argument on the request wrapper so you can never forget it, and set it per call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/hmrcApi.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`application/vnd.hmrc.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;+json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "5.0"&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;hmrcConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFraudHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more trap: versions get withdrawn. Obligations used to answer on v2.0; that now returns a 404, not a 406, so it looks like a missing resource rather than a stale version. When an HMRC call 404s, check the version before you go hunting for a bad path.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. You have to send HMRC a fingerprint of the device on every call
&lt;/h2&gt;

&lt;p&gt;This one is wild the first time you read the docs. HMRC requires a set of "fraud prevention headers" on every API call: a couple of dozen &lt;code&gt;Gov-Client-*&lt;/code&gt; and &lt;code&gt;Gov-Vendor-*&lt;/code&gt; headers describing the device, the network, the screen, the timezone, and your own server. It is anti-fraud telemetry, and it is mandatory.&lt;/p&gt;

&lt;p&gt;The surprise inside the surprise: the connection method decides which headers are legal. A server-mediated web app and a server-mediated mobile app send different sets, and sending the wrong one is an error, not a nice-to-have. So I branch on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config/hmrc.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MOBILE_APP_VIA_SERVER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEB_APP_VIA_SERVER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// mobile REQUIRES the device user-agent, and must NOT send browser headers&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gov-Client-User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mobileUa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// web sends the browser JS user-agent, and must NOT send Gov-Client-User-Agent&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gov-Client-Browser-JS-User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserUa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a great free tool that saves you here: HMRC's &lt;a href="https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/txm-fph-validator-api" rel="noopener noreferrer"&gt;Test Fraud Prevention Headers API&lt;/a&gt;. It tells you exactly which header is missing or malformed. Build against it from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Quarterly updates are cumulative, not "this quarter"
&lt;/h2&gt;

&lt;p&gt;You file four times a year, so the obvious model is "four quarters, four payloads, each holding that quarter's numbers." That is wrong, and it is the kind of wrong that passes your first test and then quietly corrupts everything.&lt;/p&gt;

&lt;p&gt;Each quarterly update is &lt;strong&gt;cumulative&lt;/strong&gt;: year-to-date from 6 April, not the delta for the quarter that just closed (&lt;a href="https://www.gov.uk/guidance/use-making-tax-digital-for-income-tax/send-quarterly-updates" rel="noopener noreferrer"&gt;gov.uk guidance&lt;/a&gt;). The clue is in the HTTP method and the path. You do not POST a quarter. You PUT a summary keyed by the tax year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// PUT /individuals/business/self-employment/{nino}/{businessId}/cumulative/{taxYear}&lt;/span&gt;
&lt;span class="c1"&gt;// turnover/expenses are running totals since 6 April, NOT just this quarter&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmrcApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitQuarterlyUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nino&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;businessId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-27&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;periodDates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;periodStartDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-04-06&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;periodEndDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2027-01-05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;periodIncome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;periodExpenses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;costOfGoods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11000&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it is a &lt;code&gt;PUT&lt;/code&gt; keyed by the year, it is idempotent. Resending the same snapshot is a no-op, and a correction is just a fresh snapshot of the whole year. The practical rule I landed on: never keep a running counter you increment each quarter. Recompute the year-to-date total from your transaction ledger every time. Derive, do not accumulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The OAuth state token is single-use, and the redirect URI has to match to the character
&lt;/h2&gt;

&lt;p&gt;The OAuth flow itself is standard Authorization Code, but two details are stricter than I expected.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;state&lt;/code&gt; token. I treat it as single-use and delete it the moment it comes back, then check it has not expired. This is normal CSRF hygiene, but HMRC's sandbox is unforgiving about replays, so a "works once, fails on refresh/back-button" bug is easy to create if you leave the state lying around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/auth.ts&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deleteStateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// single-use: delete immediately&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;stateToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;redirectError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization request has expired. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the &lt;code&gt;redirect_uri&lt;/code&gt;. It has to match what you registered on the HMRC Developer Hub exactly. Same scheme, same host, same path, trailing slash and all. A mismatch does not warn you, it just fails the token exchange. I keep mine in one config value and reuse it for both the authorize URL and the token POST so the two can never drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The sandbox is fussy in ways production never warns you about
&lt;/h2&gt;

&lt;p&gt;Two sandbox gotchas ate hours, and both are about request shape rather than data.&lt;/p&gt;

&lt;p&gt;A GET with &lt;code&gt;Content-Type: application/json&lt;/code&gt; and no body gets rejected by HMRC's CloudFront edge as a &lt;code&gt;403 Bad request&lt;/code&gt;. Every read endpoint broke at once. The fix is to only set the JSON content type when there is actually a body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/hmrcApi.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// never on a bodyless GET&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror image: some POSTs that take no meaningful body 500 if you send &lt;code&gt;null&lt;/code&gt;, but accept an empty object. Triggering a calculation is the classic one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// {} not null: a null-bodied POST 500s on HMRC's calc backend, {} is accepted&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmrcApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;triggerCalculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nino&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;taxYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// sends {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you are in the sandbox, two more things worth knowing. Use the &lt;code&gt;Gov-Test-Scenario&lt;/code&gt; header to drive deterministic responses, because the no-scenario defaults can hand you stale data from old tax years that newer endpoints then reject. And wrap your calls in a retry with backoff: the sandbox throws transient 429s and 5xxs that have nothing to do with your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// retry on 429 + 500/502/503/504 + network errors, exponential backoff&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;None of this is unreasonable once you see why it exists. HMRC is moving the entire UK tax base onto software, and the friction is mostly anti-fraud and backwards-compatibility showing through the API surface. The trick is to stop reasoning by analogy with friendlier APIs: read the version off each endpoint, lean on the header validator, model state instead of events, and treat the sandbox as a strict reviewer rather than a forgiving toy.&lt;/p&gt;

&lt;p&gt;If you are about to start an MTD integration: pin versions per endpoint, build against the fraud-header validator on commit one, make your submissions idempotent, and budget a day for the sandbox to teach you its quirks. That is most of the pain, paid down up front.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments if you are wrangling MTD too.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Solomon Amos is the founder of TapTax, a Making Tax Digital app for UK sole traders. He built TapTax's HMRC integration, spent two years embedded at HMRC's digital programmes, and holds a PhD in machine learning. The code above is from TapTax's own integration; this is an engineering write-up, not tax advice. &lt;a href="https://www.linkedin.com/in/solomonudoh/" rel="noopener noreferrer"&gt;linkedin.com/in/solomonudoh&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>lessons</category>
    </item>
  </channel>
</rss>
