Extension Packages
Some integrations are products in their own right — bank feeds with provider credentials, e-invoicing against government APIs. REDA Financials structures these as extension packages: the core defines a global abstract contract, the extension implements it, and a setup record wires the two together. Two base contracts ship today, plus three optional capability interfaces a provider implements only if it needs them.
How the pattern works
Both contracts follow the same architecture, and both class headers describe it in the same words: "The core package resolves the provider at runtime via: Type.forName(setup.Provider_Class__c).newInstance()". Neither contract hard-codes a vendor. There are four moving parts, each with a clear owner:
| Part | Owner | Role |
|---|---|---|
Abstract contract — refin.BankFeedProvider / refin.EInvoiceProvider | Core package | Global abstract classes (both marked @since 2026-05-28) defining the method signatures and result shapes. Global = part of the stable managed-package surface. |
| Implementation | Extension package (or your org) | A concrete class extending the contract; holds all provider-specific API logic. Per both class docs: "Authentication and API credentials are managed entirely by the extension package." |
Setup record — refin__Bank_Feed_Setup__c / refin__E_Invoice_Setup__c | Your administrator | Registers the fully qualified implementing class in Provider_Class__c, plus environment, activation and scope settings. |
Result object — BankFeedResult / EInvoiceResult | Core package | The global inner class every method returns, so outcomes are recorded uniformly whatever the provider. |
refin.BankFeedProvider — automated bank statements
- The contract: three methods an implementation provides —
connect(the provider's linking flow),sync(fetch transactions since the cursor and deliver statement lines),disconnect(revoke access). Results return in a standard shape (success, transactions imported, updated cursor, balance, error). - The wiring: a Bank Feed Setup record per connected account — provider name, environment (Sandbox/Production), the implementing class name, sync cursor and status (Active / Paused / Error / Disconnected), last sync. The setups live on the bank account's record page.
- Credentials stay with the extension — the core never sees provider tokens; the extension owns its authentication end-to-end.
- The payoff: statements arrive as ordinary bank statements (import source marked accordingly) and flow into the same matching and reconciliation as a CSV would — automation upstream, familiar workflow downstream.
The bank feed contract, method by method
Every method receives the setup record as its context. The class doc spells out the model: "Each method receives the Bank_Feed_Setup__c record as the primary context — the provider reads the linked bank account and its own credentials to execute sync logic."
global abstract BankFeedResult sync(refin__Bank_Feed_Setup__c setup);
global abstract BankFeedResult connect(
refin__Bank_Feed_Setup__c setup,
Map<String, String> params
);
global abstract void disconnect(refin__Bank_Feed_Setup__c setup);
| Method | What the contract asks of it (from the class documentation) |
|---|---|
sync | "The provider fetches new transactions from the bank API (using the sync cursor from the setup record), converts them to Bank Statement Lines via the core package's internal import path, and returns the updated cursor for persistence." The doc adds it is "Called by the core scheduled batch job" — see the orchestration caveat above; the scheduling side is core-version-dependent. |
connect | Initiates the linking flow for a new bank account. "Provider-specific: may involve OAuth redirect, token exchange, or API key validation. The params map carries provider-specific connection parameters (e.g. public_token for Plaid, link_id for Belvo)." |
disconnect | "Revokes access tokens, removes webhooks, and cleans up provider-side resources. The core package updates the setup record's Sync_Status to Disconnected after this method completes." |
BankFeedResult is a global inner class — every member is @AuraEnabled global, and the constructor defaults success to false and transactionsImported to 0, so an implementation must earn a success:
| Member | Type | Meaning |
|---|---|---|
success | Boolean | Whether the operation completed. Defaults to false. |
transactionsImported | Integer | How many transactions the sync delivered. Defaults to 0. |
updatedCursor | String | The new incremental position, returned "for persistence" so the next sync resumes where this one ended. |
currentBalance | Decimal | The provider-reported account balance at sync time. |
errorMessage | String | Honest failure detail when success is false. |
refin__Bank_Feed_Setup__c — field reference
| Field | What it holds |
|---|---|
Bank_Account__c | The REDA bank account this feed serves — one setup per connected account. |
Provider_Class__c (label "Provider Class") | The heart of the wiring. Description verbatim: "Fully qualified Apex class name that extends refin.BankFeedProvider… Extension packages register their implementation here (e.g. plaid.PlaidBankFeedProvider)." |
Provider__c | Display name only — per its own description, "The actual integration logic is determined by Provider_Class__c, not this field." |
Environment__c | Which provider environment the connection targets (Sandbox / Production). |
Is_Active__c | Whether the feed participates in syncing. |
Sync_Cursor__c | The provider's incremental position — sync reads it, and returns the updated value in BankFeedResult.updatedCursor. |
Sync_Status__c | Picklist: Active / Paused / Error / Disconnected. The Error value's help is explicit: "Error: last sync failed — check Audit Log for details." |
Last_Sync__c | When the feed last synchronised. |
Provider_Account_Id__c | The provider-side identifier for the linked account. |
A skeleton implementation
Here is the shape of a provider — Meridian's team sketching one against their US bank's direct API. Only the contract pieces are real; the comments mark where your provider logic goes:
global class MeridianBankFeedProvider extends refin.BankFeedProvider {
global override refin.BankFeedProvider.BankFeedResult connect(
refin__Bank_Feed_Setup__c setup, Map<String, String> params) {
refin.BankFeedProvider.BankFeedResult result =
new refin.BankFeedProvider.BankFeedResult();
// Exchange the connection parameters (e.g. public_token) with the
// provider. Store credentials in YOUR package — never in core objects.
result.success = true;
return result;
}
global override refin.BankFeedProvider.BankFeedResult sync(
refin__Bank_Feed_Setup__c setup) {
refin.BankFeedProvider.BankFeedResult result =
new refin.BankFeedProvider.BankFeedResult();
// 1. Fetch transactions since setup.refin__Sync_Cursor__c
// 2. Deliver them as bank statement lines
// 3. Return the new cursor so it can be persisted
result.transactionsImported = 42;
result.updatedCursor = 'cursor-after-this-batch';
result.success = true;
return result;
}
global override void disconnect(refin__Bank_Feed_Setup__c setup) {
// Revoke tokens and webhooks on the provider side; the core
// sets Sync_Status to Disconnected after this completes.
}
}
refin.EInvoiceProvider — statutory e-invoicing
- The contract:
submit(build the jurisdiction's payload from the document and call the authority),checkStatus(poll asynchronous authorities),cancel(jurisdiction-specific rules apply — the contract note itself cites Brazil's 24-hour window and Mexico's receiver acceptance). - The wiring: an E-Invoice Setup per entity/country — provider, environment, implementing class, which document types participate, and an Auto Submit On Post option; every submission attempt is recorded as an E-Invoice Trail on the document (status, fiscal UUID and number, payloads, response, retry count) — the compliance audit trail regulators expect.
- Availability note: the trails and related lists are visible throughout the app (you've seen E-Invoice Trails on invoice pages); live submission requires the provider extension for your jurisdiction — confirm what's available for your country with REDA.
The e-invoice contract, method by method
| Method | Signature | Contract |
|---|---|---|
| submit | global abstract EInvoiceResult submit(refin__E_Invoice_Trail__c trail, SObject sourceDocument) | The source document is a refin__Sales_Invoice__c or refin__Purchase_Invoice__c; the provider builds the jurisdiction's XML/JSON and calls the government API; the core updates the Trail from the result |
| checkStatus | global abstract EInvoiceResult checkStatus(refin__E_Invoice_Trail__c trail) | Polling for asynchronous authorities that answer later |
| cancel | global abstract EInvoiceResult cancel(refin__E_Invoice_Trail__c trail) | "Jurisdiction-specific rules apply (e.g. Brazil NF-e cancellation within 24h, Mexico CFDI cancellation with receiver acceptance)." |
All three return an EInvoiceResult — members: success, status, fiscalUUID, fiscalNumber, responseCode, responseMessage, xmlPayload, responsePayload, errorMessage.
The e-invoice objects — field reference
refin__E_Invoice_Setup__c field | Role |
|---|---|
Entity__c · Country_Code__c · Environment__c · Is_Active__c | Which company, which jurisdiction, test vs production, on/off |
Provider__c | Display name only |
Provider_Class__c | The fully qualified Apex class extending refin.EInvoiceProvider (the field's own example: sovos.SovosEInvoiceProvider) |
Document_Types__c | Multi-select: Sales Invoice / Credit Memo / Purchase Invoice / Vendor Memo — the "core package checks this field when determining whether to create an E-Invoice Trail record" |
Auto_Submit_On_Post__c | Verbatim: "When checked, posting a Sales Invoice or Purchase Invoice automatically creates an E-Invoice Trail record in Draft status, triggering the asynchronous submission process. When unchecked, e-invoice submission must be initiated manually via a quick action or API call." |
The refin__E_Invoice_Trail__c record is the per-submission audit row: lookups to the setup and the source invoice, Document_Type__c, Status__c, Fiscal_UUID__c, Fiscal_Number__c, XML_Payload__c, the response fields (Response_Code__c/Message__c/Payload__c/Date__c), Submitted_Date__c, Retry_Count__c and Provider_Document_Id__c — the evidence chain regulators expect, one row per attempt. Status__c carries eight values: Draft, Ready to Send, Queued, Submitted, Accepted, Rejected, Cancelled, Error.
Optional capability interfaces — how a frozen contract still grows
refin.EInvoiceProvider shipped as a global abstract class in a Managed-Released version, which means no method can ever be added to it. Rather than freeze the framework with it, REDA adds new behaviour as separate global interfaces, which Salesforce does permit. A provider extends EInvoiceProvider and implements whichever capability interfaces it supports; the core detects support with instanceof and simply skips the capability for providers that don't implement it.
| Interface | Methods | Implement it when |
|---|---|---|
refin.EInvoiceQueueProvider | drain(setup, maxRecords) returning List<EInvoiceEvent>; acknowledge(setup, events) | Your tax authority answers through a notification queue rather than a synchronous response. One drain carries both directions — outbound status updates and inbound supplier documents — discriminated by EInvoiceEvent.processType. There is deliberately no separate "fetch inbound" method. |
refin.EInvoiceDocumentProvider | fetchDocuments(trail) returning List<EInvoiceDocument> | You retrieve the legal artefacts (signed XML, authority response, a printable representation such as Brazil's DANFE) on demand rather than delivering them inline on the event. Skip it if your events already carry documents. |
refin.EInvoiceProvisioner | provisionOrganization(setup, args); provisionCompany(setup, args) — both returning EInvoiceProvisionResult | You support self-service provisioning of the authority-side account structures from inside the app. This is a later-phase capability — today those structures are created manually by the compliance provider. It is declared now precisely because a released global API can't be extended later. |
acknowledge only once each event is durably stored — acknowledging first would lose the notification permanently. A provider must not enable any auto-acknowledge mode that pre-empts that call, and drain must return only unacknowledged notifications so repeated drains make forward progress.
The three data types they exchange
refin.EInvoiceEvent— one milestone in a document's life at the authority. The core correlates it back to a Trail byerpDocumentId, a value your provider stamps into the outbound envelope and the authority echoes back.statusis normalised to Accepted, Rejected, Cancelled or InProgress — the core keys retry and terminal-state logic off that, never off the rawresponseCode. It also carriesnotificationId,processType(Outbound / Inbound),responseMessage,fiscalUUID,fiscalNumber, an optionaldocumentslist,payloadandeventDate.refin.EInvoiceDocument— one artefact:title,fileExtension('xml' or 'pdf'),base64Body, anddocumentKind('Legal', 'LegalResponse' or 'Representation'). The core decodes it and attaches it to the Trail and the source invoice as Salesforce Files, so users see the compliant artefacts on the record they already work with.refin.EInvoiceProvisionResult—success(defaulting tofalse),externalId(the provider-side handle) andmessage.
documentKind, processType and status are all String rather than enums — deliberately, so the permitted set can grow without breaking a frozen global signature. Don't switch exhaustively on them; handle the values you know and pass the rest through.
Common mistakes to avoid
| Mistake | Why it hurts | Do this instead |
|---|---|---|
Typos in Provider_Class__c | Runtime resolution is by name — a wrong string means the provider simply never runs | Copy the fully qualified name (namespace.ClassName) from the extension's docs; test the connection immediately |
Relying on Provider__c to switch providers | It's a display label; "The actual integration logic is determined by Provider_Class__c, not this field." | Change Provider_Class__c; update the label for humans |
| Storing authority/bank credentials in the setup records | The contract is explicit: "Authentication and API credentials are managed entirely by the extension package" | Credentials live in the extension's own named credentials, governed by your org's security |
| Building your org's code against a provider package's internals | Provider packages evolve on their own schedule — only refin. global contracts are the stable seam | Integrate through the setup records and trails; treat the provider as a black box |
| Assuming orchestration details without testing your version | Scheduling and auto-dispatch behavior is version-dependent | Verify sync scheduling and auto-submit behavior in a sandbox on your installed versions before promising them to the business |
For teams building an extension
- Implement the abstract class in your package's namespace; register the fully-qualified class name in the setup record.
- Own your credentials (named credentials in your package), your API mapping, your error semantics — return honest results; the core surfaces them in Sync Status and trails.
- Build against the contract only — the documented global surface is the stable one (the rules).
Where to go next
- Importing Bank Statements — the flow feeds land in.
- AppExchange Integrations — the broader ecosystem picture.
- Apex Development on REDA — the global-surface rules.
Frequently asked questions
The contract is designed for the aggregator ecosystem (Plaid, Belvo and peers are the archetypes it names). Ask REDA which provider extensions are current for your region — availability evolves faster than documentation.
The contract resolves the implementing class by name, so an org-local implementation is technically addressable — sensible for a bespoke bank API. Package it properly if it'll outlive one org.
Country-by-country (Brazil, Mexico, Italy, Saudi Arabia lead; many follow). Your tax adviser owns the "must we"; the setup + provider extension owns the "how".
BankFeedResult reports success, transactionsImported, updatedCursor (where the next sync resumes), currentBalance and errorMessage — defaults are honest (success=false, zero imported), so a lazy provider can't accidentally report victory. EInvoiceResult carries the authority's answer: fiscal identifiers, response payloads, and the error text when it refused.
It's the connection's health at a glance — Active / Paused / Error / Disconnected. The field's own guidance for the bad state: "Error: last sync failed — check Audit Log for details." Disconnected is set by the core after your provider's disconnect completes.
Was this article helpful?