FinBaze for Platforms

Concepts

For your companies FinBaze files the returns itself: VAT, ICP, OSS, corporate income tax, annual accounts, wage tax and the pension declaration, and for the UK VAT and Income Tax. It only does so with the company's authorisation. The company's own user gives it, in your app, in front of a text that names FinBaze B.V. as the filer and your platform only as "via {your platform}". Your backend cannot give it for them.

Who files#

FinBaze files as a software supplier: it is not a tax intermediary and has no BECON number. For a company on your platform that has no accountant connected, FinBaze submits the return on the company's behalf under the company's own authorisation: the return is filed in the company's name as the taxpayer (Bedrijf), with no intermediary number in it, and FinBaze's filing desk as the contact for questions. The stored consent record is the legal basis.

  • A company whose accountant files a tax (see working with accountants) is filed by the accountant, with the firm's own BECON number, instead.
  • A request that only an intermediary may make, such as the corporate income tax extension (nl_cit_uitstel), is refused for a company without an accountant with FILER_MUST_BE_INTERMEDIARY. It is a blocked reason (taxFilingSubmitPreview.blockedReason), not a failure: show that the company's accountant files it.
  • FinBaze staff never file for your companies, also not under a support grant.

Who does what#

Embed or headless session (the company's admin user)Your server tokenFinBaze staff
Read the statement, grantyes (grantTaxAuthorisation)no, FORBIDDEN_TOKEN_TYPEno
Read status and historyyesyesyes
Revokeyesyes (for instance when you offboard a company)yes, on written instruction

A user token minted by your backend (createPlatformUserToken) cannot grant either. The session must include the onboarding or the tax-authorisation component, and the user must be an admin of the company. A company of another platform answers 404.

In the embedded components the consent is already there:

  • onboarding shows a "Filing authorisation" step while a tax_<tax>_authorisation requirement is currently_due (see below). Switch it off per session with components.onboarding.features.filingConsent: false.
  • tax-authorisation shows the status per tax, the history, a grant for what is missing and a revoke per tax with a dialog that says what it does ("FinBaze will no longer submit your Dutch VAT returns; you can still prepare them"). features.grant and features.revoke switch the buttons off.
  • vat-return, icp-return, oss-return show the grant panel in place of an error while FinBaze may not file yet (features.inlineConsent; without it they send the user to the tax-authorisation component with a navigate event). When the company's accountant files the tax they say so.

The components emit tax_authorisation.granted and tax_authorisation.revoked with { companyId, taxes, channel, stepUp, at }, the payload of the webhook.

If you build your own screens with the headless hooks, useTaxAuthorisation() gives you status, statement(taxes), grant({ statement }), revoke() and history():

const { status, statement, grant } = useTaxAuthorisation();
const shown = await statement(["nl_vat", "nl_icp"], "nl-NL"); // render shown.title and shown.paragraphs
await grant({ statement: shown });                             // sends shown.version and shown.hash back

What the user agrees to#

taxAuthorisationStatement(companyId, taxes, locale) returns the statement for the chosen taxes: a title, the paragraphs (one per tax: what FinBaze files, when, that the user reviews before submitting; what revoking does; that an accountant who files a tax files it instead) and a hash. The grant sends back the statementVersion and statementHash it displayed. FinBaze renders the same text again and refuses any difference with TAX_AUTHORISATION_STATEMENT_OUTDATED, so the stored record proves the exact text, the language and the taxes. Statements are versioned (nl-2026-10, en-2026-10). A statement that changes meaning is published as a new version: existing grants show as reconfirmation_due and stay valid for a grace period (60 days) while the user confirms again.

Taxes#

TaxCovers
nl_vatVAT returns, corrections (suppletie), objections, EU VAT refund requests
nl_icp, nl_ossICP and OSS returns
nl_citCorporate income tax return, extension, provisional assessment, objection
nl_annual_accountsAnnual accounts
nl_wage_tax, nl_pensionPayroll tax returns and the pension declaration (UPA)
uk_vat, uk_income_taxUK VAT and MTD Income Tax. The HMRC connection stays as it is; this is FinBaze's authority to file
uk_annual_accountsUK annual accounts at Companies House. Registered when the company's annual accounts file at Companies House (a UK company, LLP or qualifying partnership), not by a tax obligation

A grant can only name taxes the company has an obligation for (TAX_NOT_REGISTERED otherwise).

Status, history and revoking#

query { taxAuthorisationStatus(companyId: "…") { tax state firmName } }

state is active, missing, reconfirmation_due or delegated_to_firm (a firm engagement covers the tax; the firm files it, firmName says who). Direct FinBaze companies have no consent and get no rows.

query { taxAuthorisations(companyId: "…", includeRevoked: true) { id taxes channel grantedAt stepUp revokedAt revokedVia supersedesId grantedBy { externalUserId } } }

mutation { revokeTaxAuthorisation(input: { companyId: "…", taxes: [nl_icp], reason: "offboarding" }) { id taxes revokedAt } }

The history is append-only. A revoke for some of a grant's taxes revokes that grant and writes a new one for the rest (supersedesId) that keeps the original evidence. Records are never deleted. A revoke is effective on the next request: the next submit for that tax fails with TAX_AUTHORISATION_REQUIRED. Drafts stay.

await finbaze.platform.taxAuthorisations.status(companyId);
await finbaze.platform.taxAuthorisations.list(companyId, { includeRevoked: true });
await finbaze.platform.taxAuthorisations.revoke(companyId, { taxes: ["nl_icp"], reason: "offboarding" });

There is no grant in the server SDK, on purpose.

Requirements#

For each active obligation that maps to one of these taxes, onboardingRequirements lists tax_<tax>_authorisation (for example tax_nl_vat_authorisation; in the design notes this is tax.<tax>.authorisation) as currently_due while the tax is missing or reconfirmation_due. It is absent once active or delegated_to_firm. When it changes you get company.requirements.updated.

Step-up#

If your step_up provider lists the action tax_authorisation.grant (see delegated actions), the first grantTaxAuthorisation fails with STEP_UP_REQUIRED and a challenge. The embedded components hand it to your page (onStepUp) and retry with your signed assertion; with the headless hook the error carries extensions.challenge and you call grant again with stepUp: { challengeId, jws }. The record keeps the satisfied challenge (stepUp: true).

Events#

EventWhendata.object
tax_authorisation.grantedA user authorised FinBaze{ companyId, taxes, channel, stepUp, at }
tax_authorisation.revokedA revoke wrotethe same; channel is embed, headless, platform, admin or lifecycle
company.requirements.updatedThe requirements of a company changedthe company; data.details.reason is tax_authorisation

No IP address or user agent is in a webhook; they are in the audit log. FinBaze sends the user nothing about a revoke itself: the message type tax_authorisation.revoked goes through your message routing (partner_delivers by default).

Errors#

TAX_AUTHORISATION_REQUIRED (a submit without consent), TAX_AUTHORISATION_STATEMENT_OUTDATED, TAX_NOT_REGISTERED, STEP_UP_REQUIRED, FORBIDDEN_TOKEN_TYPE, FILING_DELEGATED_TO_FIRM, FIRM_NOT_AUTHORISED_FOR_TAX and FILER_MUST_BE_INTERMEDIARY. See errors.