Research date: 2026-09-03
Scope: API references, auth approaches, data model mapping, and integration options for a future Exact Online / AFAS direct-connection feature (MoSCoW "Should", per docs/data-audit-engine-requirements-source.md requirements "Directe koppeling met Exact Online" and "Directe koppeling met AFAS").
Companion docs: docs/auditfile-nu-research.md (XAF parser gaps), docs/xas2027-srs-reconciliation.md (XAS payroll data), docs/data-audit-engine-requirements-source.md (product requirements)
Note on verification: Exact Online REST API and AFAS App Connector information below is based on their public developer documentation and published resources as known at research date. Items flagged [unverified — confirm with live API] require a test environment to validate before implementation.
Athena currently ingests GL data only via XAF file upload or CSV import. Both paths require the user (accountant or client contact) to manually export a file from their ERP and upload it. Direct ERP integration eliminates this friction:
The two Dutch market leaders in the SME + MKB + accountants segment where Athena operates are Exact Online and AFAS Profit. Together they hold the majority of the Dutch accounting software market covered by Athena's target client base (as noted in docs/sra-research.md).
Exact Online is Exact Software's cloud ERP/accounting platform. Relevant products:
| Product | Target segment | Notes | |---|---|---| | Exact Online Accounting | SME, accountancy bureaus | Primary target for Athena integration | | Exact Online for Accountants | Accounting bureaus managing multiple clients | Multi-division portal — one login, N client companies | | Exact Globe / Synergy | Large enterprise, on-premises | Separate API; not in scope for MVP |
Each client company in Exact Online is a division (administratie). One Exact Online account (accountancy bureau) can manage many divisions. Division number is a required path parameter in every API call.
Exact Online uses standard OAuth 2.0 Authorization Code flow. No other auth method is supported for third-party apps.
Developer portal: https://apps.exactonline.com — register an app to obtain client_id + client_secret.
OAuth endpoints (NL instance):
| Step | URL |
|---|---|
| Authorization | https://start.exactonline.nl/api/oauth2/auth |
| Token exchange | https://start.exactonline.nl/api/oauth2/token |
| Token refresh | Same token endpoint with grant_type=refresh_token |
Scopes: Exact Online uses a single implicit scope covering all API access for the registered app. No fine-grained OAuth scopes per endpoint.
Token lifetime: Access token expires in 10 minutes. Refresh token expires in 30 days (sliding). The refresh token must be stored securely and re-used to obtain new access tokens. If the refresh token is not used for 30 days the user must re-authorize.
Multi-division: After OAuth login the user selects a division via the Exact Online UI; the selected division ID is returned in the OAuth callback or must be queried from GET /api/v1/-/current/Me (returns {CurrentDivision} among other fields). All subsequent GL calls include this division in the path: /api/v1/{division}/....
Integration implication for Athena:
{access_token, refresh_token, expiry, division_id} per connected Athena dossier.Base URL (NL): https://start.exactonline.nl/api/v1/{division}/
Documentation: https://support.exactonline.com/community/s/knowledge-base → "REST API" section. Full endpoint reference available in Swagger-style browser at https://start.exactonline.nl/api/docs.
Protocol: JSON over HTTPS. OData v3 query syntax ($filter, $select, $top, $skip, $orderby).
Rate limiting: ~60 requests/minute per OAuth token (per division). Bulk sync endpoints are preferred for large data pulls (see 2.4).
GET /api/v1/{division}/financial/GLAccounts
Relevant response fields:
| Exact Online field | Type | Athena mapping |
|---|---|---|
| ID | GUID | Internal ID |
| Code | String | GeneralLedgerAccountDto.accountId |
| Description | String | GeneralLedgerAccountDto.accountDescription |
| Type | Int (enum) | GeneralLedgerAccountDto.accountType (mapped from enum — see 2.6) |
| RGSCode | String | GeneralLedgerAccountDto.rgsCode |
| IsBlocked | Bool | No Athena field; use to filter inactive accounts |
OData example: $select=Code,Description,Type,RGSCode&$top=1000
Two endpoints:
1. Standard (paginated, max 1000 rows per call):
GET /api/v1/{division}/financialtransaction/TransactionLines
2. Bulk sync (preferred for full fiscal-year pulls, returns up to 1M rows via server-side cursor):
GET /api/v1/{division}/sync/Financial/TransactionLines
The sync endpoint accepts a $synctoken to retrieve only changes since last pull — enables incremental refresh.
Relevant fields per transaction line:
| Exact Online field | Type | Athena mapping |
|---|---|---|
| FinancialYear | Int | Filter parameter (fiscal year) |
| FinancialPeriod | Int | TransactionDto.periodNumber (once XAF real-file gap is fixed) |
| EntryNumber | Int | TransactionDto.transactionId |
| LineNumber | Int | Transaction line identifier |
| GLAccount | GUID | Join to GL account ID |
| GLAccountCode | String | TransactionDto.accountId |
| Date | DateTime | TransactionDto.transactionDate — includes time component (see DA-092 note below) |
| AmountDC | Decimal | Amount in division (reporting) currency |
| AmountFC | Decimal | Amount in foreign currency (if applicable — see DA-095) |
| CurrencyCode | String | Foreign currency code (see DA-095) |
| Description | String | TransactionDto.description |
| Type | Int (enum) | Journal type (Bank=20, Cash=21, General=22, Purchase=40, Sales=50, etc.) — maps to XAF jrnTp |
| JournalCode | String | Journal identifier (jrnID) |
| JournalDescription | String | Journal name |
| UserName | String | TransactionDto.createdBy — not available in XAF; enables DA-007/DA-019 |
| Modified | DateTime | Last modification timestamp |
| AmountVAT | Decimal | VAT amount on the line |
| VATCode | String | VAT code (enables DA-042 BTW analysis) |
DA-092 key finding: Date is a DateTime (not xs:date) — the time component is present in the Exact Online API response and can enable after-hours posting detection that is impossible from XAF 3.2. This is the data Athena needs to implement DA-092 without a separate ERP connection; the XAF standard gap (DA-092 Candidate status) is resolved by pulling from the API directly.
GET /api/v1/{division}/financial/Journals
Returns journal definitions with Code, Description, Type (enum: 10=Opening, 20=Bank, 21=Cash, 22=General/Memorial, 25=Correction, 40=Purchase, 50=Sales). Used to build the jrnTp → type label mapping for DA-041 (Balans per Dagboek) and DA-051 (Memorial Entries Viewer).
GET /api/v1/{division}/crm/Accounts
Returns all relations (debtors + creditors + other). Fields: AccountCode, Name, IsCreditAccount, IsSupplier, BankAccounts (linked IBAN collection). The IBAN association enables DA-098 (counterparty IBAN cross-check) with structured data rather than regex-on-description.
Exact Online can generate a XAF file on demand via the application's own export function. There is no public REST endpoint to programmatically trigger XAF generation and download — the export is performed by the user in the Exact Online UI and then uploaded to Athena.
Implication: Direct API integration (sections 2.4) provides richer, more structured data than the XAF file export path, particularly for posting timestamps, user identity, and VAT code detail. The XAF export path remains useful as a fallback for clients not willing to grant OAuth access.
Exact Online GLAccount.Type enum → Athena accountType:
| Exact Online Type code | Dutch label | Athena accountType | |---|---|---| | 10 | Winst & Verlies (Omzet) | W&V / Omzet | | 11 | Winst & Verlies (Kosten) | W&V / Kosten | | 20 | Balans (Activa) | Activa | | 22 | Balans (Passiva) | Passiva | | 30 | Overig | Overig |
Athena currently uses accountType string values derived from XAF <accTp> codes (P=profit&loss, B=balance). The Exact Online type codes are more granular and would allow Athena to distinguish revenue from cost accounts without additional user configuration — enabling DA-031 (Revenue-Account Filter) and DA-032 (Expense-Account Filter) automatically.
Exact Online provides a webhook subscription API:
POST /api/v1/{division}/webhooks/WebhookSubscriptions
Supported events include financial.TransactionLines (fires when transactions are posted). This enables Athena to receive push notifications when new postings occur, allowing a "refresh now" trigger without polling — relevant to the "Real-time synchronisatie met ERP" requirement (currently Won't — MVP, but enabled by webhook infrastructure).
AFAS develops Profit, a Dutch-market ERP used heavily in accounting, HR, and payroll. Relevant products:
| Product | Notes | |---|---| | AFAS Profit (cloud) | Enterprise ERP; most commonly encountered in larger SMEs and accounting bureaus | | AFAS Boekhouden (Small Business) | Smaller entities; XFC/XAF export is the primary data path — see DA-038 |
AFAS Profit exposes data via a connector-based REST API. Unlike Exact Online's resource-oriented REST API, AFAS uses pre-defined "GetConnectors" (read) and "UpdateConnectors" (write) that the AFAS administrator configures in their Profit environment. Standard Profit installations ship with a set of default connectors, but connector availability varies by installation and AFAS version.
AFAS Profit REST API uses App Connector authentication. There is no OAuth 2.0 flow; authentication is a stateless token-per-request approach.
Documentation: https://help.afas.nl/help/NL/SE/App_Cnnctr_Add.htm
How it works:
{ "token": "<the-app-token>", "version": 1 }
Authorization header:Authorization: AfasToken <base64url-encoded-json>
No expiry, no refresh — the token is valid until revoked by the AFAS administrator. This is simpler than OAuth but has implications:
Integration implication for Athena:
Base URL: https://{bedrijfsnummer}.rest.afas.online/ProfitRestServices/connectors/{connector-name}
Where {bedrijfsnummer} is the 5-digit AFAS company number (e.g., 12345).
Protocol: JSON over HTTPS. AFAS uses a proprietary query/filter syntax, not OData.
Filter syntax:
GET /connectors/{name}?filterfieldids={field}&filtervalues={value}&operatortypes=1
More complex filters use an XML <Filters> payload passed as a query parameter.
Pagination: skip and take parameters. Default page size 100; maximum 1000 per request [unverified — confirm with live API].
Important caveat: AFAS connector names and available fields depend on the specific Profit version and what connectors the AFAS administrator has configured. The names below reflect common standard Profit connectors for the GL domain; a given installation may use different names or may have renamed them. Athena must discover available connectors via GET /connectors (returns a list of all configured connectors) before assuming specific names exist.
Connector discovery:
GET https://{bedrijfsnummer}.rest.afas.online/ProfitRestServices/connectors
Returns JSON list of all GetConnectors in the environment. Use this at connection setup to validate that required connectors are present.
Connector: typically Gl_Account or GlAccount [unverified — confirm connector name with live AFAS instance]
Expected fields:
| AFAS field | Athena mapping |
|---|---|
| Gb_Reknr / GLAccount_Code | GeneralLedgerAccountDto.accountId |
| Gb_Omschrijving / GLAccount_Description | GeneralLedgerAccountDto.accountDescription |
| Gb_Soort / AccountType | GeneralLedgerAccountDto.accountType |
| RGS_Code | GeneralLedgerAccountDto.rgsCode |
AFAS Profit separates journal entries into multiple connectors depending on journal type:
| Connector (typical name) | Content |
|---|---|
| Gl_MemJournalEntry or Gb_Memo_Jrn | Memorial (handmatige memoriaalboekingen) |
| Gl_BankJournalEntry | Bank journal entries |
| Gl_PurchaseJournalEntry | Purchase journal entries |
| Gl_SalesJournalEntry | Sales journal entries |
Alternatively, a single Gl_TransactionLine connector (if configured) may expose all transaction lines across journals. This varies by Profit version and configuration.
Expected fields per transaction line:
| AFAS field (typical) | Athena mapping |
|---|---|
| Jaar / Year | Fiscal year filter |
| Periode / Period | Period number |
| Transactietype / EntryType | Journal type |
| Volgnummer / EntryNumber | TransactionDto.transactionId |
| Rekeningnummer / GLAccountCode | TransactionDto.accountId |
| Datum / Date | TransactionDto.transactionDate — DateTime in AFAS Profit; time component available — enables DA-092 |
| Bedrag_DC / Amount_DC | TransactionDto.debitAmount / creditAmount (sign-based) |
| Bedrag_FC / Amount_FC | Foreign currency amount (DA-095) |
| Valuta / Currency | Currency code (DA-095) |
| Omschrijving / Description | TransactionDto.description |
| Gebruiker / UserName | TransactionDto.createdBy — not available in XAF; enables DA-007/DA-019 |
| Mutatiedatum / MutationDate | Entry creation timestamp |
Sign convention [unverified — confirm with AFAS]: AFAS typically returns amounts as signed values where debit is positive and credit is negative (or vice versa — depends on account type). Athena must apply sign normalization when building debitAmount / creditAmount from a signed AFAS amount field.
Connector: typically Contact or Debn_Crkn (debiteuren/crediteuren)
Provides counterparty name, address, IBAN, and type (debtor vs. creditor). Enables DA-098 structured IBAN cross-check.
AFAS Profit generates XAF files via the "Auditfile Financieel" export function in the application UI. Unlike Exact Online, there is no API endpoint to trigger XAF generation programmatically — the export is a manual UI action. The result is an .xaf or .xfc file that the user uploads to Athena. This path is already addressed by DA-001 (XAF ingestion) and DA-038 (XFC compressed ingestion).
| Dimension | Exact Online | AFAS Profit | |---|---|---| | Auth protocol | OAuth 2.0 (standard) | Proprietary AfasToken header | | User setup friction | User logs in via browser — no admin steps | AFAS administrator must create App Connector and supply token to Athena | | Token lifecycle | Access token: 10 min; refresh token: 30 days | No expiry until revoked | | Multi-company | Handled via division selection in OAuth flow | One base URL per company number — separate token per company | | Token storage security | Refresh token (medium sensitivity) | App token (high sensitivity — long-lived static credential) | | Documentation quality | Comprehensive Swagger docs + community | Detailed but connector-name variability makes scripting harder |
| Data field | XAF 3.2 | Athena (current) | Exact Online API | AFAS Profit API |
|---|---|---|---|---|
| GL account code | <accID> | accountId | GLAccountCode | Gb_Reknr / GLAccount_Code |
| GL account description | <accDesc> | accountDescription | Description | Gb_Omschrijving |
| GL account type | <accTp> (P/B enum) | accountType | Type (int enum, more granular) | Gb_Soort |
| RGS code | <leadCode> | rgsCode | RGSCode | RGS_Code |
| Transaction date | <trDt> (xs:date) | transactionDate (LocalDate) | Date (DateTime — has time) | Datum (DateTime — has time) |
| Transaction amount | <amnt> + <amntTp> | debitAmount / creditAmount | AmountDC (signed) | Bedrag_DC (signed) |
| Foreign currency | <curCode> / <curAmnt> | missing | AmountFC + CurrencyCode | Bedrag_FC + Valuta |
| Journal type | <jrnTp> (B/M/I/V/K) | missing (uses manualEntry flag) | Type int enum (20=Bank, 22=General, etc.) | Transactietype |
| Posting user | not in XAF standard | createdBy (fixture only) | UserName | Gebruiker |
| Posting timestamp | not in XAF standard | not in DTO | Date (full DateTime) | Datum (full DateTime) |
| VAT code | <amntTp> (no VAT code) | missing | VATCode | Varies by connector |
| Counterparty IBAN | <iban> in <customerSupplier> | not parsed | In crm/Accounts/BankAccounts | In relations connector |
| Counterparty name | <custSupName> | not parsed | AccountName | In relations connector |
Summary: Both ERP APIs provide significantly richer data than the XAF standard, specifically:
Athena adds an "ERP Connection" concept per dossier: the accountant provides OAuth credentials (Exact) or App Token (AFAS), and Athena pulls GL accounts + transaction lines directly from the ERP API at analysis time (or on demand via a "Refresh" button).
Architecture sketch:
Dossier → ErpConnectionConfig (type=EXACT|AFAS, credentials encrypted)
→ ErpIngestionService (pulls via REST, transforms to TransactionDto + GLAccountDto)
→ existing analysis pipeline (unchanged)
Pros:
Cons:
Athena remains file-based but adds a "Generate and upload" button that calls the ERP's own auditfile export function on behalf of the user, then uploads the resulting XAF file to Athena. Not feasible for Exact Online or AFAS — neither ERP exposes a programmatic XAF generation API (export is UI-only in both products). This option is ruled out.
Athena ingests the XAF file for the full GL population (all transactions), then optionally fetches enrichment fields (posting timestamp, user, VAT code) from the ERP API for the same period using the transaction's period/number as a join key.
Pros:
Cons:
<nr> (transaction number) and Exact/AFAS EntryNumber — needs validation with real data [unverified]Recommendation: Start with Option A (full API pull) for new dossiers. Option C (hybrid) is a valid transitional step if XAF parsing is fixed first and ERP API connectivity is optional. Option B is not feasible.
The following backend changes are needed to expose ERP-sourced data through Athena's analysis pipeline:
| Change | Purpose | Blocking DA checks |
|---|---|---|
| Add transactionTimestamp (LocalDateTime) to TransactionDto | Store ERP posting time | DA-092 |
| Add currencyCode (String) + foreignCurrencyAmount (BigDecimal) to TransactionDto | Foreign currency support | DA-095 |
| Add vatCode (String) to TransactionDto | VAT code per line | DA-042 |
| Add counterpartyId (String) + counterpartyName (String) + counterpartyIban (String) to TransactionDto | Structured counterparty data | DA-098 |
| createdBy field already exists — confirm it maps to ERP user field | Posting user identity | DA-007, DA-019, DA-093 |
| Add ErpConnection entity to backend | Persist OAuth token/AFAS token per dossier | — |
| Add ErpIngestionService (Exact) + AfasIngestionService | Pull and transform ERP data | — |
These changes to TransactionDto are additive — existing XAF/CSV ingestion paths set these fields to null, and null-safe checks in analysis services handle missing data gracefully.
Exact Online connector name for transaction lines: The sync endpoint sync/Financial/TransactionLines is the preferred bulk pull path. Confirm this is available on the NL instance and does not require additional Exact API partner certification. The standard financialtransaction/TransactionLines endpoint has a 1000-row page limit and would require full pagination for large GL populations.
AFAS connector name variability: The GL connector names listed above (Gl_Account, Gl_MemJournalEntry, etc.) are typical but not guaranteed — a live AFAS Profit instance must be queried via GET /connectors to confirm available connector names before implementation. Athena's AFAS integration must handle connector discovery dynamically rather than hardcoding names.
AFAS sign convention for amounts: Whether AFAS returns amounts as signed (positive debit, negative credit) or unsigned with a separate D/C indicator needs confirmation from a live instance [unverified].
Join key reliability (Option C hybrid): Whether the XAF <nr> field reliably matches the EntryNumber in Exact Online / AFAS API for the same transaction is unconfirmed. This needs a test with a real XAF export and the corresponding API response from the same ERP instance.
AFAS Small Business / Boekhouden vs Profit: AFAS Small Business (formerly AFAS Boekhouden) is a simpler product that may not expose the App Connector REST API at all — its primary export path is XFC (covered by DA-038). Confirm whether AFAS Small Business exposes the same REST API or whether the integration applies to AFAS Profit only.
Exact Online EU instances: The base URL start.exactonline.nl is the NL instance. Exact Online also has BE (start.exactonline.be) and DE/other instances with separate OAuth endpoints. Athena should treat the instance URL as a configurable parameter rather than hardcoding the NL URL.
API partner certification (Exact Online): Exact Online may require an app to be listed in the Exact App Center (apps.exactonline.com) before it can be authorized by end users. This may involve a review process. Confirm whether a private/internal app registration (without App Center listing) is sufficient for Athena's use case.
| Resource | URL | Notes |
|---|---|---|
| Exact Online REST API docs | https://support.exactonline.com/community/s/knowledge-base | REST API section; Swagger at https://start.exactonline.nl/api/docs |
| Exact Online OAuth guide | https://support.exactonline.com/community/s/knowledge-base → "OAuth authentication" | Step-by-step OAuth 2.0 flow |
| Exact Online App registration | https://apps.exactonline.com | Register app → get client_id + client_secret |
| Exact Online sync endpoints | https://support.exactonline.com/community/s/knowledge-base → "Bulk export / sync" | sync/Financial/TransactionLines documentation |
| AFAS App Connector setup | https://help.afas.nl/help/NL/SE/App_Cnnctr_Add.htm | Official AFAS App Connector help page |
| AFAS REST API documentation | https://help.afas.nl/help/NL/SE/Prof_Tech_AIS_REST.htm | REST API protocol, filter syntax, pagination |
| AFAS connector reference | Varies by Profit version — access via Profit management console | Lists available GetConnectors and their field definitions |
| docs/auditfile-nu-research.md | (this repo) | XAF parser compliance gaps — context for why direct API is richer than XAF |
| docs/data-audit-engine-requirements-source.md | (this repo) | MoSCoW requirements including "Directe koppeling met Exact Online/AFAS" |
Reacties