Integrations
Twinfield SOAP API: 23 gotchas that drop VAT silently
A ranked cheatsheet of 23 Twinfield SOAP gotchas from a Zwolle invoice-chase rollout, starting with the ones that return 200 OK and quietly lose the VAT line.

It is 23:40 on a Tuesday in Zwolle. The night-shift bookkeeper at a 41-person accountancy is double-checking yesterday's outbound invoices. A UK customer's invoice catches her eye: total €4,200, VAT line missing, reverse-charge remark missing too. The Twinfield envelope our agent received that afternoon said <result>1</result>. Our log said OK.
That invoice is now legally wrong. The customer paid it. The VAT return will be wrong if nobody catches it. We caught it. Then we found twenty-two siblings.
We have been running an invoice-chase agent on top of Twinfield for that firm since March. Twinfield is the system of record for most Dutch SMB bookkeeping; if you have automated against it, you know the shape of the pain. If you have not, here is what we wish someone had handed us on day one: twenty-three gotchas, ranked by how quietly they hurt you.
The 200 OK is the boss enemy
Twinfield's SOAP API will happily return <result>1</result> on an envelope that is technically valid, internally well-formed, and semantically wrong. There is no separate warnings channel. There is the result code, and there is the messages array, which is sometimes populated with informational lines you can ignore and sometimes populated with the one line that means we silently dropped your VAT.
The principle is general: confident wrong answers carry more liability than visibly broken ones. An obvious 500 routes itself to a queue. A mis-posted invoice routes itself to a tax auditor.
Tier 1: the eight that silently drop VAT
These return 200 OK. They look like wins. They are not.
1. Empty or missing <country> on the customer dimension. Twinfield's VAT engine reads the customer's country from the dimension, not from the invoice. If the field is blank, the engine assumes no determination possible and omits the VAT line entirely instead of failing. Make country a required field in your customer-sync, not a nice-to-have.
2. Header <vatcode> overwritten by line <vatcode>. Many examples online set the VAT code at the invoice header level. Twinfield accepts it. It will also overwrite it with whatever is on each <line> element, defaulting to the line-item template's stored code if you did not set one. Set it on the line. Always.
3. Reverse-charge codes look like the regular EU code. A B2B EU customer needs VL or VH (depending on the office's chart), not VN. Twinfield does not validate that the customer's VAT number actually exists in VIES. It posts whatever you give it. The invoice goes out without VAT and without the legally required BTW verlegd remark, because that legal text comes from the template, triggered by the VAT code you forgot to set.
4. The customer's stored default VAT code overrides the invoice payload. If you set a default <vatcode> on the customer dimension and do not explicitly override it on the line, the dimension wins. This is the opposite of what most ERPs do. It bites hardest when sales reps update customer records by hand between agent runs.
5. Cached EU VAT number on a now-non-EU customer. Post-Brexit UK customers whose <vatnumber> field still contains a GB code that an old office configuration recognises as EU will get EU treatment. The fix is to clear the field, not to update it.
6. <perfdate> lands in a closed VAT period. Twinfield silently moves the VAT line to the next open period without telling you. Your invoice document and your VAT return now disagree, and nobody finds out until the quarter closes.
7. Currency mismatch silently zeroes the VAT amount. Send <currency>USD</currency> to an office whose base is EUR, and Twinfield's amount-based VAT mode will compute VAT against zero. Percentage-based mode handles it. Most older offices are still amount-based.
8. SubAdministration journal without a VAT mapping. If your invoice posts to a journal that is not linked to a VAT system, Twinfield posts the invoice and just does not generate a VAT line. No error. The journal selector dropdown in their UI does not flag this either.
Each of these returns <result>1</result>. Each of these will pass an automated end-to-end test that checks for success. You need to validate the envelope content, not the result code.
Tier 2: the seven that fail loudly enough to queue
These at least give you something to alert on.
9. Session-cluster mismatch. Twinfield's auth response includes a <cluster> URL. You must use that URL for subsequent calls, not the URL you logged in against. Use the wrong one and you get Invalid session ID on the second call. The login host is login.twinfield.com; the cluster hosts look like accounting1.twinfield.com or accounting4.twinfield.com. Do not hardcode them; read them per session.
10. Wrong office code posts to a different company. The <office> element in the session selects which company file you are writing to. Get it wrong and your invoice posts to a real, different client's books. Twinfield will not check that the office matches the customer. Hardcode the office in environment config, never in a payload that a model can hallucinate.
11. Session timeout returns 200, not 401. Sessions die after about twenty minutes of inactivity. The next call returns a 200 OK envelope containing Not logged in in the messages array. Anything that handles this with if response.status_code == 200: success will silently drop every operation after the timeout.
12. Dimension type is DEB for customers, not CRD. CRD is creditors (your suppliers). Swap them and you create an invoice against your own supplier ledger. This one does error, but the message is dimension not found, which sends most engineers off hunting for a missing record instead of a typo.
13. Duplicate invoice number in the same period. Twinfield rejects with transaction number already exists. If you are regenerating numbers from your own DB, handle the case where Twinfield's counter has moved on while your agent was sleeping.
14. Date format must be YYYYMMDD, no separators. 2026-06-11 gets you a parse error that looks unrelated. The error references the field name, not the format problem.
15. Attachments need <attachment> not <file>. The element name differs between processes. Base64-encoded PDF body, no chunking required up to about 8 MB. The wrong tag returns a useful error; the wrong base64 returns a 200 with a corrupted blob attached to the invoice.
Tier 3: the eight that just eat your day
16. read.transactions paginates at 100 rows with no warning. Loop on <offset> until you get an empty page.
17. Dutch decimal comma versus dot: Twinfield accepts both, then rounds them differently in edge cases involving currency conversion. Send dots, always, regardless of office locale.
18. <memo> truncates at sixty characters without notice. Anything longer is silently cut.
19. <invoicedate> after period close is a soft warning during the month and a hard error after the 10th of the following month.
20. WSDL URLs in old documentation point to accounting.twinfield.com. The current endpoints are login.twinfield.com for OAuth and <cluster>.twinfield.com for SOAP. Wolters Kluwer's own pages still link to dead URLs in places.
21. OAuth refresh tokens expire after ninety days of disuse. If your agent runs daily this is fine. If you test every two months, plan for a manual re-authorisation step.
22. Rate limiting is per-session, not per-IP. A runaway agent does not lock out the rest of your infrastructure, but it does lock out itself for the next several minutes. Back off exponentially on 429.
23. The XSD for <sales> is not the same as for <salesinvoice>. They look similar. They differ in three field names. If you copied a snippet from a Stack Overflow answer dated before 2019, you have the old one.
The minimum viable safety net
After the 23rd one we stopped fixing them individually and built a guard. It runs after every post, before our agent logs OK. Skeleton:
def validate_posted_invoice(envelope_xml, expected):
soup = parse(envelope_xml)
if soup.find('result').text != '1':
raise PostFailed(soup.find('messages').text)
# The eight silent VAT killers
vat_line = soup.find('line', {'type': 'vat'})
if expected.vat_due and vat_line is None:
raise SilentVATLoss('200 OK but no VAT line generated')
if expected.reverse_charge:
remarks = (soup.find('remarks') or {}).get('text', '').lower()
if 'verlegd' not in remarks:
raise SilentVATLoss('reverse-charge remark missing')
if expected.currency != soup.find('currency').text:
raise CurrencyMismatch()
posted_period = soup.find('period').text
if posted_period != expected.period:
raise PeriodDrift(f'expected {expected.period}, got {posted_period}')
return True
The guard adds about 40 ms per invoice and catches every Tier 1 gotcha we know about. We re-read the posted document instead of trusting the response envelope. Trust the ledger, not the API call.
If your only test for a Twinfield post is the result code, you have no test. Read the document back, compare it to what you intended to send, and alert on the diff.
What to do tomorrow
Open your Twinfield integration, find every place you check result == 1 and treat it as success, and add one extra call: read the document you just posted, parse the VAT line, compare it to what your agent intended. That is a one-afternoon change and it catches all eight Tier 1 gotchas. The legal exposure of one mis-VATted invoice is bigger than the day of engineering.
When we built the invoice-chase AI agent for that Zwolle accountancy, the first gotcha we hit was number one: country codes blank on customers who had been in the system since 2014. We ended up fixing the data, not the agent, because the agent was right and the ledger was wrong. That is roughly the shape of every integration we ship: the wrapper is small, the data hygiene is most of the work.
Key takeaway
Trust the posted document on Twinfield SOAP, not the response code. A 200 OK envelope routinely returns success while silently dropping the VAT line.
FAQ
Why does Twinfield return 200 OK on a failed VAT calculation?
The SOAP result code reflects whether the envelope was processed, not whether the business logic produced what you expected. You have to read the posted document back to verify the VAT line and remarks are there.
Which Twinfield gotcha causes the most damage in production?
Empty customer country codes. The VAT engine silently omits the VAT line instead of failing, so invoices go out legally wrong with no alert. Make country mandatory on every customer dimension you sync.
How do you safely keep a Twinfield session alive?
Always call subsequent operations against the cluster URL returned by the auth response, never the login URL. Treat a 200 OK envelope containing 'Not logged in' as a re-auth signal, not a success.
Is the Twinfield REST API a way out of these gotchas?
Partly. Auth and pagination are nicer, but the VAT engine and the silent-drop behaviour are the same under both transports. The safety net still applies: re-read what you posted before logging success.