Integrations
Twinfield API quirks: 15 silent traps behind a 200 OK
Twinfield's API returns 200 OK while quietly dropping cost centers on multi-administration tenants. Fifteen quirks from a Rotterdam agent rollout.

The Slack message arrived at 14:47 on a Thursday. "Twinfield says the invoices posted. The Twinfield UI shows them. The cost center is empty on 73 of them." We were three weeks into rolling out an invoice-coding agent at a Rotterdam bookkeeping firm with 84 client administrations. The agent had been running for nine days, classifying scanned PDFs, extracting line items, and posting them to Twinfield via the SOAP transaction API. Every response had come back 200 OK. Every transaction was queryable in the UI. The dimensions we'd attached to each line item, the ones that map to cost centers and projects, were gone on roughly a third of the posts.
This is the cheatsheet we wish we'd had on day one. Fifteen Twinfield SOAP and REST quirks, ranked by how quietly they break things on multi-administration tenants. Number one is the silent cost-center drop. The rest descend from there, in roughly the order they'll cost you sleep.
If you're integrating Twinfield this quarter, build a checklist around these. If you're shipping an autonomous agent against it, treat every 200 OK like a maybe.
The silent-drop tier
These return 200 OK and leave you debugging for a day. They share a pattern: Twinfield validates what it understands and discards what it doesn't, without raising a fault.
1. Dimension codes that don't exist in the target administration
Cost centers (the dim2 field), projects (dim3), and any custom dimension are scoped per administration, not per tenant. If you POST a transaction line with <dim2>KP-040</dim2> and KP-040 doesn't exist in that specific office, the line saves without the dimension. No fault, no warning, no msg attribute on the line. The transaction is "valid" because dim2 is technically optional in the schema. Each of the Rotterdam firm's 84 administrations had its own cost-center catalogue, inherited from whichever previous bookkeeper had set up the file. Our agent was sending head-office codes into every one of them.
2. Dimensions that exist but are blocked or inactive
Twinfield lets bookkeepers block dimensions after year-end, or mark them inactive for new postings. A blocked dimension on a posted line: silent drop. Same status code, same response shape. The block status doesn't show up in the standard dimension-list endpoint unless you explicitly ask for inactive entries.
3. VAT codes that exist but aren't linked to the transaction type
Each transaction type (sales invoice, purchase invoice, journal, bank) has an allowlist of VAT codes inside the administration's settings. Send a valid VAT code that isn't allowlisted for purchase invoices and the line posts with VAT code null. The lines still balance because Twinfield computes the VAT-exclusive amount as the full line value. We caught this only because the monthly VAT return stopped tying out by about 0.4%.
4. Line numbers that collide inside a multi-line transaction
In manual document-number mode, if you reuse a linenumber value inside a transaction, the later line overwrites the earlier one. No collision error.
<!-- Quietly drops line 1 -->
<line type="detail">
<linenumber>1</linenumber>
<dim1>4000</dim1>
<value>100.00</value>
</line>
<line type="detail">
<linenumber>1</linenumber>
<dim1>4001</dim1>
<value>200.00</value>
</line>
5. Hierarchy levels that don't match the administration's setup
Some administrations use only dim2. Some use dim2 + dim3. Some define a custom dimension at dim4 for project codes. The SOAP schema accepts all of them on every line. If the target administration doesn't have dim3 enabled, your dim3 value disappears. Twinfield returns 200 OK and a clean-looking transaction.
The cluster and session tier
Twinfield's auth model is its own subculture. None of these will give you a 200 OK that lies. They will, however, eat your morning if you're discovering them in production.
6. The cluster URL is per session, not per tenant
When you log in via SOAP, the response contains a <cluster> URL (for example https://c3.twinfield.com). All subsequent calls must go to that URL, not the login URL, and not whichever cluster you got yesterday. If you hardcode c3 because that's what worked last week, you'll be talking to a session on a different cluster today and getting Logon failed despite a fresh token.
7. OAuth refresh tokens rotate on every use
The REST API uses OAuth2 with rotating refresh tokens. Every refresh returns a new refresh token, and the old one is invalidated immediately. If two workers refresh concurrently, one of them is holding an invalidated token within milliseconds. We serialised refreshes behind a single per-environment mutex after the second outage.
8. SelectCompany is not as idempotent as you'd hope
Calling SelectCompany switches the active administration for the rest of the session. Forget to call it after a logon and operations go to the user's default office. The default office on a service account is rarely the one you want, and the API will happily post a Rotterdam client's invoice into the head-office books.
9. Concurrent sessions per user are capped
The cap isn't documented prominently. It sits around 10 to 20 depending on contract. Hit it and you get Maximum number of concurrent sessions exceeded. Pool sessions per administration and reuse them. Don't open one per HTTP request.
The shape-of-the-payload tier
10. SOAP returns 200 OK with a SOAP Fault in the body
This is canonical SOAP, but it surprises engineers who arrived from REST. A <soap:Fault> element inside a 200 response is still an error. Parse the XML before treating the call as successful.
resp = requests.post(cluster_url, data=envelope, headers=headers)
if resp.status_code != 200:
raise TwinfieldError(resp.text)
root = ET.fromstring(resp.text)
ns = {"s": "http://schemas.xmlsoap.org/soap/envelope/"}
fault = root.find(".//s:Fault", ns)
if fault is not None:
raise TwinfieldError(fault.find("faultstring").text)
11. The transaction-write response has per-line result attributes
A transaction can save with result="1" overall while individual lines have result="0" and a msg attribute. We've seen this when one of three lines failed validation and Twinfield wrote the other two. Walk the response tree. Don't trust the top-level result.
12. Date format is YYYYMMDD in SOAP, ISO-8601 in REST
Mix them up and SOAP coerces invalid dates to the current period's first day. Silently. You'll find out when the bookkeeper asks why every invoice from May posted on the 1st.
13. Field-name casing differs between SOAP and REST
invoiceNumber in REST, invoicenumber in SOAP. Office codes are case-sensitive on writes but tolerant on some reads. We standardised on uppercase office codes everywhere after an agent wrote to office nl001 when the canonical was NL001. Same office in some lookups, different office in others.
The coverage-gap tier
14. The REST API does not cover what the SOAP API covers
As of mid-2026, journal-line dimension manipulation, several master-data writes, and a handful of reporting endpoints are SOAP-only. The Twinfield webservices documentation is the source of truth for what each surface supports. The REST docs imply more parity than actually exists. Plan for a hybrid client and pick the surface per call, not per project.
15. Periods must be open, and the error doesn't name the administration
Posting into a closed period returns a clear error, but the message references "period status" without telling you which administration's period is closed. When you're iterating across 84 administrations, log the office code with every error or you'll be hunting blind across cluster logs.
On any multi-administration Twinfield rollout, write a per-administration smoke test that posts a one-line transaction with a known cost center, reads it back, and asserts the cost center survived. Run it nightly across every office. The silent drops in tier one will not show up in any other monitor you already have.
What we changed in the agent
After the cost-center drop surfaced, we put three layers in front of every write:
- A pre-flight resolver that looks up every dimension, VAT code, and period status against the target administration's master data, and refuses to send the transaction if any reference is unresolved. Lookups are cached per administration with a 10-minute TTL.
- A read-back step after every write that re-fetches the transaction by its returned number and diffs the dimensions, VAT codes, and per-line amounts against the intended payload. Drift triggers a Slack alert that includes the office code, transaction number, and the field that drifted.
- A circuit breaker per administration. If read-back drift exceeds 2% over a rolling hour, the agent pauses writes to that office and queues the rest for human review.
The third layer is the lesson from the broader agent conversation right now. The pattern under most agent post-mortems is the same: an agent that kept acting when it should have stopped. For accounting integrations the cost of acting wrongly is concrete and auditable, so the bar isn't "halt on error." It's "halt on uncertainty." A read-back diff is the cheapest uncertainty signal we've found.
On the orchestration side, libraries like Burr model agent behaviour as an explicit state machine, and that framing has changed how we wire integrations. Each Twinfield write transitions from drafted to posted to verified, and only verified closes the loop. posted without verified means the API said yes but we haven't proved the fields landed. That distinction is the whole game.
Treat every Twinfield 200 OK on a multi-administration write as provisional until you've read the record back and diffed the fields that matter. The API will save what it understands and drop what it doesn't.
Closing
When we built the invoice-coding agent for the Rotterdam firm, the thing we kept running into was that Twinfield's idea of "valid" is narrower than your idea of "correct." We ended up solving it with the pre-flight plus read-back loop above, which now sits inside every AI agent we ship against bookkeeping APIs.
The smallest useful thing you can do today, if you're starting a Twinfield integration this month, is write the per-administration smoke test from quirk one and put it on a 15-minute cron. The day it goes red is the day you'll know exactly which administration's cost-center catalogue drifted, and which agent run made the mess.
Key takeaway
Treat every Twinfield 200 OK on a multi-administration write as provisional until you've read the record back and diffed the fields that matter.
FAQ
Does Twinfield's REST API cover everything the SOAP API does?
Not as of mid-2026. Journal-line dimension manipulation, several master-data writes, and some reporting endpoints remain SOAP-only. Plan for a hybrid client and pick the surface per call.
Why does Twinfield return 200 OK when it drops my cost center?
Dimension fields are technically optional in the transaction schema. If the code you sent doesn't exist or is blocked in that administration, the line saves without it. Always read the transaction back and diff dimensions.
How do I handle Twinfield OAuth refresh tokens across multiple workers?
Refresh tokens rotate on every use, so concurrent refreshes invalidate each other. Serialise refreshes behind a single mutex per environment, or run them through a dedicated token-broker process.
What's the safest way to roll out a Twinfield integration across many administrations?
Build a per-administration smoke test that posts a one-line transaction with a known cost center, reads it back, and asserts the dimension survived. Run it on a 15-minute cron and alert on any drift.