Twinfield – Gotchas
Twinfield (Sage) cloud accounting platform.
28 gotchas across 16 resources
These are connector-specific behaviors and limitations to be aware of when integrating.
Bank Accounts1 gotcha
bankAccountsAllListed 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.
Bank Feed Statements1 gotcha
bankFeedStatementsAddMaps 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
billPaymentsAllTwinfield 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.
billPaymentsAddDisabled 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
billsAllThis 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.
billsAddBoth 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.
billsOnetotal_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
customersAllstatus 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
expensesAllThis 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.
expensesAddaccount_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.
General Ledger Transactions1 gotcha
generalLedgerTransactionsAllThis 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
invoicesAllThis 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.
invoicesAddline_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.
Journal Entries2 gotchas
journalEntriesAllThis 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.
journalEntriesAddTwinfield 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.
Ledger Accounts2 gotchas
ledgerAccountsAlltype, 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.
ledgerAccountsAddTwinfield 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 0–3 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.
Payment Methods5 gotchas
paymentMethodsAllTwinfield'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.
paymentMethodsAddPayment 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.
paymentMethodsOneTwinfield'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.
paymentMethodsUpdatePayment 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.
paymentMethodsDeletePayment 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
paymentsAllTwinfield 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.
paymentsAddDisabled 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
projectsAllstatus 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
suppliersAllstatus 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
taxRatesAllListing 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.
Tracking Categories1 gotcha
trackingCategoriesAllstatus 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.