Twinfield – Gotchas

Service ID: twinfield

Twinfield (Sage) cloud accounting platform.

⚠️

28 gotchas across 16 resources

These are connector-specific behaviors and limitations to be aware of when integrating.

allbankAccountsAll

Listed via Twinfield's Finder service (Search, type BNK) rather than ProcessXml — ProcessXml's <list><type>banks</type></list> and <read><type>bank</type></read> both reject as unimplemented server-side ("Type niet geïmplementeerd", confirmed live), but Finder(BNK) works and is live-verified (returns real bank/cash codes for this office, e.g. "BNK"/"ABN-Amro 789", "KAS"/"Kas").

Finder only returns a code and a name per row — account_type, currency, balance, available_balance, iban and status are not available from this connector.

name/bank_name are read from Twinfield's own column header for that office's configured UI language (e.g. "Naam" for a Dutch office). Not verified against a non-Dutch office (this build only has access to a Dutch-language tenant), but the mechanism is confirmed live — a non-Dutch office would need the equivalent column name mapped instead.

addbankFeedStatementsAdd

Maps onto Twinfield's <statement target="electronicstatements"> SOAP document, which is distinct from the transaction-posting mechanism used by payments/bills/journal-entries and does support importing transaction lines with no GL account or invoice match (contraaccount/contraiban are left empty).

Live-verified structurally: a real write reaches Twinfield, is accepted as well-formed, and is validated against real bank records (confirmed via a deliberately invalid IBAN, which Twinfield rejected by name: "Er bestaat geen bank met IBAN ..."). Not yet verified end-to-end with a real IBAN for this connector, since Twinfield has no bank-feed-account listing endpoint to source one from (see the bank-feed-accounts connector note) — bank_feed_account_id must be supplied by the caller as the target account's IBAN, with no way to validate it client-side beforehand.

source_transaction_id on read is synthesized ({iban}-{statement_number}-{line_index}) because Twinfield's statement transaction lines carry no client-supplied identifier of their own.

The statement currency defaults to EUR when not set on the connection — the unified model carries no per-statement or per-transaction currency field to source it from instead. Statements for a non-EUR bank account must have the connection's currency setting configured accordingly.

Twinfield requires a currency and a statementnumber per statement with no equivalent unified field; currency defaults to the connection's configured currency (or EUR), and statementnumber is derived from start_date. Twinfield does report an importduplicate flag on the response when a statement number was already imported, which is not yet surfaced on the unified response.

update and delete are not implemented and are genuinely impossible, not merely unimplemented: Twinfield's own documentation states "once a bank statement is created, it is not possible to change it."

one targets a completely different SOAP service (BankStatementService.svc, a "Command & Query" framework distinct from the ProcessXml/Finder services this connector otherwise uses, authenticated via an <Authentication><AccessToken>/<CompanyCode> header rather than ProcessXml's <Header>, and requiring a SOAPAction HTTP header WCF uses for dispatch — ProcessXml/Finder don't check it). That service has no "get by number" query — only date-range queries — so the connector queries the single day encoded in the id and picks the matching statement (by IBAN + statement number) out of the result.

Known broken, not yet working. Live-tested directly against Twinfield (bypassing this connector) with several request variants — with and without a duplicate legacy <Header> alongside <Authentication>, and with both the office code and the organisation GUID as CompanyCode — all return the same InternalServiceFault, a generic WCF fault with no detail (IncludeExceptionDetailInFaults is off on Twinfield's server). This is a different fault type from the SecurityFault/ActionNotSupported faults seen earlier in the same investigation, meaning auth and SOAP routing succeed and the failure is inside Twinfield's query-handling logic — but which part of the request body it's rejecting could not be narrowed further without Twinfield-side exception tracing. The response mapping (field names Iban, Number, StatementDate, Lines.BankStatementLine[].Amount/Description/ContraAccountName, etc., sourced from Twinfield's published XSD, not an observed real response) has therefore never been exercised against a real response and should be treated as unverified.

Bill Payments2 gotchas

allbillPaymentsAll

Twinfield returns every matching bill payment in a single call for this endpoint — there is no server-side pagination, so a large date range can be a large response.

account and allocations are not populated on the list. Fetch the individual bill payment by id to get those fields.

addbillPaymentsAdd

Disabled as of 2026-08-18. Same underlying constraint as the payments resource: Twinfield's bank-daybook transaction requires header.number to reference an existing, already-imported bank statement line, which this connector has no way to discover (its bank-statement-import path, bankFeedStatementsOne, is itself disabled), and account.id needs a real ledger-account code rather than the bank BOOK/daybook code /accounting/bank-accounts returns. See the payments resource's gotcha for the live reproduction.

Bills3 gotchas

allbillsAll

This connector fetches every matching bill in a single call — there is no server-side pagination, so a large date range can be a large response. The id returned matches what bills.get expects, and other transaction types (bank, journal entries) are excluded.

sub_total, total_tax, line_items and updated_at are not available from this list view (Browse Data returns one summary row per bill, not the underlying line items); total is derived assuming the connector's own no-VAT-on-bills limitation (see the post gotcha) still holds, so it doubles as sub_total would if this list mapped it. balance and paid_date, notably, ARE available here even though they are always null on GET by id — Twinfield's transaction read omits the balancing control-account line where that data lives, but Browse Data exposes it directly.

addbillsAdd

Both supplier.id and bill_number are required — Twinfield rejects a bill without an invoice number, and every line must post against the accounts-payable control account, which requires knowing the supplier.

The accounts-payable control account itself is per-organisation configuration Twinfield exposes no way to look up. It defaults to 1600 (Twinfield's standard chart of accounts); if this office uses a different code, configure it on the connection.

reference is not sent — Twinfield's purchase transaction header has no free-text field distinct from the invoice number (invoicenumber, already used for bill_number) to hold it. Treat reference as write-unsupported for this connector.

line_items[].tax_rate is not sent. Twinfield accepts a vatcode per line and will compute VAT automatically, but the transaction's balancing total line must then equal the net amount plus that computed VAT — this connector does not yet calculate that (it would need the tax rate's percentage, which this connector's tax-rates resource does not currently expose). Sending vatcode without also correcting the total line fails with a balance error on every taxed bill, so it is left unmapped rather than shipped half-working. Bills post without VAT recorded on the transaction until this is built.

onebillsOne

total_tax, balance and paid_date are always null on this get-by-id operation — Twinfield's response omits the balancing line where those values would come from. sub_total is set equal to total rather than left null, since this connector does not currently post VAT on bills (see the bill create gotcha), so there is no tax to subtract. balance and paid_date are available on bills.list.

Customers1 gotcha

allcustomersAll

status is not returned by this list call — Twinfield's underlying <list><type>dimensions</type></list> only returns code and name per row. Fetch the customer by id to read it. The same applies to emails, phone_numbers, addresses, websites, tax_number, bank_accounts, updated_at and account — all empty/null on the list response, populated only when fetched by id.

Expenses2 gotchas

allexpensesAll

This connector fetches every matching expense in a single call — there is no server-side pagination, so a large date range can be a large response.

transaction_date is approximated as the first day of the expense's accounting month, not the actual transaction date. sub_total, total_tax and updated_at are not available on the list.

This list also returns records created through this connector's journal-entries resource — there is no field to tell the two apart. Treat any entry whose lines look like a multi-line journal posting as a possible journal-entries record.

addexpensesAdd

account_id (or account.id) is required for this connector even though the unified schema only requires one of them via x-apideck-strict-any-of — Twinfield has no implicit default for the credited side.

Not every ledger account works as account_id — Twinfield rejects some accounts with an error naming a conflicting bank/cash book or control account rather than the field itself. A plain, general-purpose ledger account works; a bank, cash-book-linked, or control account does not. There is no way to predict which accounts qualify from the unified request alone.

An expense posted through this connector is indistinguishable from a journal entry on the Twinfield side; there is no payment_type/cash-vs-card distinction preserved.

The write always creates a draft transaction; this connector has no way to post it directly.

delete is disabled as of 2026-08-18. It read/deleted by transaction number alone against the hardcoded MEMO daybook, but Twinfield transaction numbers are unique only PER DAYBOOK — the same number can also identify a journal-entries record, or a transaction entered directly in Twinfield's own UI, with no field this connector can use to tell them apart. Live-reproduced: deleting an "expense" by id voided an unrelated, pre-existing journal entry that happened to share its number. get has the same collision on the read side — it is non-destructive but can silently return a hand-entered journal entry reshaped as an expense (dropping any line beyond the first debit/credit pair). Do not rely on an id obtained from journal-entries or Twinfield's own UI being safe to pass to this resource, or vice versa.

memo becomes the credited line's description, and Twinfield caps a transaction line's description at 40 characters ("De omschrijving mag maximaal 40 tekens bevatten") — confirmed live: a 43-character memo was rejected outright rather than truncated. This connector does not truncate memo or line_items[].description before sending; a value over 40 characters fails the whole write.

update is not implemented, and not just for lack of time: Twinfield's transaction API has no in-place update verb, only add (always assigns a new transaction number) and delete. Faking an update as delete-then-recreate would return a different id than the one being updated, breaking the update contract, so it is left unsupported rather than built as a lossy workaround.

allgeneralLedgerTransactionsAll

This connector fetches every matching transaction in a single call — there is no server-side pagination, so a large date range can be a large response. currency_rate and reference are not available. Transactions created through this connector's expenses resource are indistinguishable from manual journal entries and are returned as source_type: journal_entry.

posted_at is approximated as the first day of the transaction's accounting period (month), not the actual posting date, since the connector's data source for this endpoint does not expose the real date.

Getting a specific transaction by id is not supported — Twinfield's transaction numbers are not globally unique across transaction types, so there is no reliable way to look one up directly.

Invoices2 gotchas

allinvoicesAll

This list only returns unpaid invoices — Twinfield does not expose a way to list paid or historical invoices. status, line_items, invoice_date, due_date, currency, sub_total, total_tax and balance are not available on the list; fetch the invoice by id for those fields.

Because of this, webhook notifications only reliably cover new invoices — once an invoice is paid it drops out of this list entirely rather than appearing as an update, so a status change to paid will not reliably produce a webhook event.

addinvoicesAdd

line_items[].ledger_account.id must reference a profit-and-loss (income or expense) ledger account — Twinfield rejects an invoice line that posts to a balance-sheet account (e.g. a bank or receivables account) with "Grootboekrekening bestaat niet (in de PNL-groep)".

line_items[].description is not preserved on the created invoice line. Twinfield derives the line's description from the ledger account it posts to, so the created invoice shows a fixed label for that ledger account instead of the text you sent.

alljournalEntriesAll

This connector fetches every matching journal entry in a single call — there is no server-side pagination, so a large date range can be a large response. An entry with multiple lines is grouped back into one record with a nested line_items array.

journal-entries.list also returns records that were created through this connector's expenses resource — there is no field to tell the two apart. Treat any entry whose lines look like a two-line debit/credit posting as a possible expenses record.

memo, posted_at and updated_at are always null on the list. Fetch the entry by id for those fields.

Twinfield transaction numbers are not globally unique — the same number can also identify a bills/expenses record, or a transaction entered directly in Twinfield, and this connector has no field to disambiguate. Getting a journal entry by an id sourced from another resource (or from Twinfield directly) can silently return the wrong record.

addjournalEntriesAdd

Twinfield caps a line's description at 40 characters. A longer value is rejected outright rather than truncated, and this connector does not pre-truncate line_items[].description before sending.

allledgerAccountsAll

type, active and status are not returned by this list call — Twinfield's underlying <list><type>dimensions</type></list> only returns code and name per row. Fetch the ledger account by id to read these fields.

This list only returns balance-sheet accounts (Twinfield's BAS dimension type). Twinfield requires a single dimtype per list call, and Profit & Loss accounts (type: "other", Twinfield's PNL dimtype) are not included — fetching, creating, updating and deleting a P&L account by id all work, but it will not appear when listing ledger-accounts.

addledgerAccountsAdd

Twinfield requires the account's dimension type (BAS for type: "balancesheet", PNL for type: "other") up front, and its own validation enforces the code format per type: balance sheet codes must match [0-3][0-9][0-9][0-9]; anything else is rejected as Profit & Loss. Send code in the matching range for the type you request — a type: "other" code inside the 03 range is rejected by Twinfield's own format check before this connector's mapping is even reached.

Fetching, updating and deleting an existing account by id derives its type from the same leading-digit rule rather than a caller-supplied value, since Twinfield's read/update/delete calls need the dimension type up front and the unified request has no type field to read it from at that point. This is Twinfield's own enforced convention, not a per-tenant setting.

allpaymentMethodsAll

Twinfield's Finder(PAY) service, the only mechanism for listing payment types, returns just code and name — there is no field for the payment method's type (cash/check/ bank_transfer/etc.) or its active/inactive status. Both are always null.

name is read from Twinfield's own column header for that office's configured UI language (e.g. "Naam" for a Dutch office). Not verified against a non-Dutch office (this build only has access to a Dutch-language tenant), but the mechanism is confirmed live.

addpaymentMethodsAdd

Payment methods cannot be created through this connector. Twinfield's docs name a ProcessXml "paymanagement" message type for maintaining pay types, but its request/ response XML shape is undocumented beyond that name — unlike Finder(PAY), which this connector reads from and which is fully specified.

onepaymentMethodsOne

Twinfield's Finder(PAY) service, the only mechanism for reading a payment type, returns just code and name — there is no field for the payment method's type (cash/check/ bank_transfer/etc.) or its active/inactive status. Both are always null.

updatepaymentMethodsUpdate

Payment methods cannot be updated through this connector. Twinfield's docs name a ProcessXml "paymanagement" message type for maintaining pay types, but its request/ response XML shape is undocumented beyond that name — unlike Finder(PAY), which this connector reads from and which is fully specified.

deletepaymentMethodsDelete

Payment methods cannot be deleted through this connector — no documented delete mechanism exists; Twinfield's docs mention only a "maintain" (create/update) message type for this data.

Payments2 gotchas

allpaymentsAll

Twinfield returns every matching payment in a single call for this endpoint — there is no server-side pagination, so a large date range can be a large response.

account and allocations are not populated on the list. Fetch the individual payment by id to get those fields.

addpaymentsAdd

Disabled as of 2026-08-18. Twinfield's bank-daybook transaction requires a header.number referencing an EXISTING bank statement line already imported into Twinfield — live-reproduced: a placeholder number fails "De boeking bestaat niet" (the booking does not exist). This is a match against a real, previously-imported statement, not a value the caller can invent, and this connector's bank-statement-import path (bankFeedStatementsOne) is itself disabled, so there is no way to discover one. Separately, account.id must be a real ledger-account code (e.g. 1010) — live-confirmed the id /accounting/bank-accounts returns (Twinfield's bank BOOK/daybook code, e.g. BNK) does not work here; the two are different Twinfield concepts this connector does not currently bridge.

Projects1 gotcha

allprojectsAll

status is not returned by this list call — Twinfield's underlying <list><type>dimensions</type></list> only returns code and name per row. Fetch the project by id to read it.

Suppliers1 gotcha

allsuppliersAll

status is not returned by this list call — Twinfield's underlying <list><type>dimensions</type></list> only returns code and name per row. Fetch the supplier by id to read it. The same applies to emails, phone_numbers, addresses, websites, tax_number, bank_accounts, updated_at and account — all empty/null on the list response, populated only when fetched by id.

Tax Rates1 gotcha

alltaxRatesAll

Listing tax rates returns only the VAT code and its description; effective_tax_rate and total_tax_rate are not populated on the list operation. Reading a single tax rate by its known VAT code does return the rate.

name and description are read from Twinfield's own column header for that office's configured UI language (e.g. "Naam" for a Dutch office) — this connector reads the Dutch column name specifically. On a non-Dutch-language office these fields would come back empty. Not verified against a non-Dutch office (this build only has access to a Dutch-language tenant), but the mechanism is confirmed live: the column name in the raw response is the office's own language, not a fixed English key.

alltrackingCategoriesAll

status is not returned by this list call — Twinfield's underlying <list><type>dimensions</type></list> only returns code and name per row. Fetch the tracking category by id to read it.