← Blog

Integrations

Twinfield, e-Boekhouden, SnelStart: 14 REST quirks

A 22-person mediabureau in Hilversum, three accounting APIs, one bonnetjes pile, and fourteen quirks we wish someone had written down before we wrote the first line of code.

Jacob Molkenboer· Founder · A Brand New Company· 16 Nov 2025· 8 min
Three manila ledgers with tabs, paper receipts tied with cord, brass paperclip, green index card, red wax seal on ivory.

Tuesday in May, fourth floor of a Hilversum mediabureau, twenty-two people on payroll, one CFO with a Trello card titled bonnetjes mei (urgent) that had been open since March. She had two part-timers scanning receipts, three administraties in Twinfield because the holding bought two production companies in 2024, a SnelStart license for the rentals BV that nobody had migrated, and one freelance bookkeeper who refused to touch e-Boekhouden ever again. We were there to build the agent that takes a photo of a taxi receipt and routes it into the right ledger, with the right BTW code and the right kostenplaats, in under nine seconds.

That worked. What did not work, on the first or second pass, was getting the three accounting APIs to agree on what a number is.

This is the cheatsheet of fourteen REST quirks we hit, ranked roughly by how much money they can quietly cost you before anyone notices. Two of them cost real euros. Five of them eat a developer-day each. The remaining seven cost only your dignity.

The two that change the numbers

1. Twinfield rounds half-to-even on line totals ending in .x5

Twinfield's REST layer (the new one, not the SOAP host you may still be on) computes BTW per line on the server when you do not send it yourself. When a line's gross total ends in a half cent, it applies banker's rounding ("round half to even") rather than commercial rounding. On a line at €12.345 gross at 21%, the netto rounds to €10.20, not €10.21. Across 800 receipts in a month, the holding's BTW return came in €4.30 short of the manually-typed sum. The auditor noticed.

The fix is to compute BTW on your side and send the rounded amounts explicitly per vatLine, rather than letting the server derive them. Twinfield will accept what you send as long as the line totals reconcile against the document total.

// Don't trust the server to round for you.
// Commercial (half-up) rounding to whole cents.
function computeLine(grossCents: number, rate: number) {
  const netto = Math.round(grossCents / (1 + rate));
  const btw   = grossCents - netto;
  return { netto, btw };
}

// Then POST netto AND btw explicitly, do not let Twinfield re-derive.
const { netto, btw } = computeLine(1235, 0.21);
await twinfield.postInvoiceLine({ netto, btw, vatCode: 'VH' });

2. SnelStart returns 200 OK while losing kostenplaats on multi-administratie POSTs

This one shipped to staging on day three and we only spotted it because one of the part-timers asked, in passing, why all the May expenses for "Studio Bussum" showed up under "Hoofdkantoor" in the dashboard. The answer is that SnelStart's inkoopboekingen endpoint accepts a kostenplaatsId field on the line item, but if the administration token in your bearer header does not match the administration the kostenplaats belongs to, the server logs the booking, returns a 200, and silently drops the kostenplaats reference. There is no validation error in the body. There is no warning header.

The fix is to refresh the administration-scoped token before every POST, even when the token still has half an hour left on the clock. The token TTL is fine. The token's administration claim is what goes stale when the user switches administrations in the SnelStart UI.

Warning

Never trust a 200 from a multi-administration boekhoud API. Read the response body. If the field you sent is not echoed back, the server quietly ate it.

The five that eat a developer-day each

These will not change a single number on a balance sheet. They will absorb a calendar day of your most expensive engineer the first time you hit them, and they will do it again the next time the team rotates.

3. Twinfield's OAuth cluster discovery

After the OAuth dance, you do not yet know which cluster (sb1, sb2, eu1, and so on) hosts the customer's data. You have to call /accesstokens against the gateway, read the twf.clusterUrl claim out of the JWT, and use that as the base URL for every subsequent request. The Twinfield webservices docs cover it, but the SDK example does not, so half the open-source wrappers on GitHub assume api.accounting.twinfield.com and break on customers in the older clusters. Decode the JWT once, cache the cluster URL alongside the refresh token, never assume.

4. e-Boekhouden REST vs SOAP session model

e-Boekhouden has two APIs. The legacy SOAP one uses OpenSession with a security code and an API token, and you must CloseSession or you burn through the daily session quota in a single afternoon. The newer REST API uses a single short-lived session token from /v1/session, but the session is bound to the IP that requested it. If your agent runs on a serverless platform with rotating egress IPs, every cold start invalidates the session and the next call returns a generic 401 with no hint that the IP changed. Pin the egress through a static NAT, or accept that you will re-authenticate roughly every minute under load.

5. SnelStart's subscription key is not the API key

SnelStart wants two headers: an Ocp-Apim-Subscription-Key from the developer portal, and a Bearer token from the OAuth flow. The portal key looks like an API key, has no expiry, and is enough to get a 401 with a misleading message ("Token invalid") that sends a new integrator down the wrong rabbit hole for half a day. The 401 means: your bearer is missing or expired. The subscription key is just the rate-limit envelope. We labelled ours SNELSTART_RATE_KEY in our secret store to stop the confusion at the source.

6. Date formats: three APIs, three answers

Twinfield wants YYYYMMDD on some endpoints and YYYY-MM-DD on others. SnelStart wants ISO 8601 with timezone and rejects naive dates. e-Boekhouden's REST endpoints want YYYY-MM-DD but the SOAP endpoints want dd-MM-yyyy. We wrote a single toBoekhoudDate(api, field, d) helper, kept it as the only place dates leave the agent, and never had a date bug after week one. You probably will not bother. You should.

7. BTW codes are case-sensitive in two of three

e-Boekhouden's REST API treats VH and vh as the same code. Twinfield rejects lowercase silently (the booking lands, the BTW code is empty, the auditor finds it three months later). SnelStart returns a clean 400. Decide the casing at the edge of your system, normalise once, and never touch it again.

The seven that cost only your dignity

None of these will keep you up at night. All of them will appear on a Friday afternoon, two minutes before a demo, and convince you for thirty seconds that the entire stack is broken.

8. Pagination: cursor on the new endpoints, offset on the old

Twinfield's REST search endpoints use cursor pagination with a continuationToken. The legacy XML browse endpoint uses offset and limit. If you build a generic iterator, write two implementations and a discriminator that picks between them per endpoint.

9. No idempotency keys, anywhere

None of the three accept an Idempotency-Key header as of June 2026. A retried POST on a flaky connection creates a duplicate booking, and your bookkeeper notices roughly never. Build your own idempotency: store a hash of (administration, document_number, line_count, total_cents) in your own database before you POST, and refuse to re-POST when the hash is present. Stripe popularised the pattern; the boekhoud APIs missed the memo.

10. Attachment size limits are not documented

Twinfield: roughly 10 MB per attachment. SnelStart: 8 MB. e-Boekhouden REST: 5 MB. Above that, the upload completes, the response is 200, and the attachment is silently truncated or missing. Compress receipts (JPEG quality 80, max edge 2000 pixels) before uploading. The downstream OCR works better on smaller files anyway.

11. Twinfield session token vs OAuth token

The legacy SOAP host uses a session token returned by SessionService. The REST host uses the OAuth access token directly. You will, at some point, paste the wrong one into the wrong header and stare at "Invalid session" for forty minutes while your colleague tells you to clear the cache. Label them clearly in your secret store. Make the variable names ugly.

12. Webhook signatures on e-Boekhouden are SHA-1

The webhook signature is HMAC-SHA1 with the shared secret. It works fine, but if your handler defaults to SHA-256 (a sensible default in 2026) you will quietly reject every event and assume the webhook is broken on their end. Re-read RFC 2104 if you have not in a while, then pin the digest algorithm explicitly.

13. SnelStart's administratie list is not stable

The order in which administrations come back from /v2/administraties is not stable across calls. Do not index by position. Match by GUID. We logged this one as a bug for two weeks before we accepted it as the spec.

14. Twinfield's office code is implicit on some endpoints

Several Twinfield REST endpoints take the office (administration) code as a path parameter. Others take it from the OAuth scope. If your token has access to exactly one office, the path parameter is optional and ignored when present. If it has access to multiple, the path parameter is required and the request 400s without it. Always send it explicitly, and your code will work in both cases.

The adapter shape that survives all three

We wrote a thin adapter per API (about 600 lines each in TypeScript), every one exposing the same six methods: listAdministraties, postInkoopboeking, attachReceipt, getBtwCodes, getKostenplaatsen, healthCheck. The agent code (the part that takes a receipt photo, runs OCR, classifies the supplier, and picks the right BTW code) talks only to the adapter interface. Quirks live behind the adapter line. If e-Boekhouden ships a v2 next year, we change one file.

The other thing we did, and you should do, is record every outbound request and its response in a small Postgres table. Not for application logging (your APM already has that). For replay. When the auditor asks why a booking has €0.01 less BTW than her spreadsheet, you have the exact payload you sent and the exact payload that came back, and you can show in twenty seconds that the difference is the server's rounding, not yours. The table cost us four hours to add and has earned them back roughly once a week.

The five-minute audit you can run today

Open a recent POST from your accounting integration in your logs. Look at the response body. Take every field you sent in the request, and check that the same field is echoed back with the same value. If kostenplaatsId, btwCode, or relatieId is missing or empty in the response, the server ate it on the way in. Write a test that asserts every field round-trips on every endpoint you POST to. That single test will catch quirk number two for you, and three more besides.

When we built the bonnetjes agent for the Hilversum mediabureau, the thing that took us by surprise was not the OCR or the supplier classification but how much of the work was teaching three accounting APIs to agree. That is the part of AI agents nobody writes the blog post about, so we did.

Key takeaway

Always read the response body. If a field you sent is not echoed back, a Dutch boekhoud API has quietly eaten it, and the 200 OK means nothing.

FAQ

Which Dutch accounting API is the cleanest to integrate with?

SnelStart's REST is the most modern but documented worst. Twinfield is the most thorough but cluster discovery and a dual API surface make it slow to start. e-Boekhouden is the cheapest and good enough for a single-administration business.

Why does Twinfield round BTW differently from my spreadsheet?

Twinfield's REST layer uses banker's rounding (half-to-even) on line-level VAT when the server derives the amounts. Pre-compute BTW in commercial rounding (half-up) on your side and POST the values explicitly via vatLine.

Can one wrapper library handle all three APIs in production?

Not one we trust as of mid-2026. Write a thin adapter per API and a shared interface in your own code. Six methods covers most expense and procurement workflows, and the adapter line is where every quirk gets contained.

How do I prevent duplicate bookings when a POST retries?

None of the three APIs accept an Idempotency-Key header. Hash administration plus document number plus line count plus total cents in your own database before each POST, and refuse to re-POST when the hash already exists.

integrationsautomationprocess automationworkflowoperationscase study

Building something?

Start a project