REDA One Help Center
EN
Language
English EN Español ES Português PT Italiano coming soon Français FR Deutsch coming soon العربية coming soon
reda.one ↗
Home / REDA Financials AI / API & Integration / Extensibility / Extension Packages

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:

PartOwnerRole
Abstract contract — refin.BankFeedProvider / refin.EInvoiceProviderCore packageGlobal abstract classes (both marked @since 2026-05-28) defining the method signatures and result shapes. Global = part of the stable managed-package surface.
ImplementationExtension 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__cYour administratorRegisters the fully qualified implementing class in Provider_Class__c, plus environment, activation and scope settings.
Result object — BankFeedResult / EInvoiceResultCore packageThe global inner class every method returns, so outcomes are recorded uniformly whatever the provider.
Contracts vs. orchestration. This article documents the extension contracts — the two global classes and their setup objects, which are the stable, published surface. How the core package drives them at runtime (scheduled sync jobs, automatic trail creation when documents post) is version-dependent and still evolving. Design and code against the contracts; confirm the orchestration behavior of your installed core version with REDA before you commit an operational design to it.

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);
MethodWhat 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.
connectInitiates 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:

MemberTypeMeaning
successBooleanWhether the operation completed. Defaults to false.
transactionsImportedIntegerHow many transactions the sync delivered. Defaults to 0.
updatedCursorStringThe new incremental position, returned "for persistence" so the next sync resumes where this one ended.
currentBalanceDecimalThe provider-reported account balance at sync time.
errorMessageStringHonest failure detail when success is false.

refin__Bank_Feed_Setup__c — field reference

FieldWhat it holds
Bank_Account__cThe 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__cDisplay name only — per its own description, "The actual integration logic is determined by Provider_Class__c, not this field."
Environment__cWhich provider environment the connection targets (Sandbox / Production).
Is_Active__cWhether the feed participates in syncing.
Sync_Cursor__cThe provider's incremental position — sync reads it, and returns the updated value in BankFeedResult.updatedCursor.
Sync_Status__cPicklist: Active / Paused / Error / Disconnected. The Error value's help is explicit: "Error: last sync failed — check Audit Log for details."
Last_Sync__cWhen the feed last synchronised.
Provider_Account_Id__cThe provider-side identifier for the linked account.
How statement lines actually land. The class doc says the provider writes transactions into Bank_Statement__c / Bank_Statement_Line__c via the core package's internal import service — but that service is not part of the global surface, so the exact statement-delivery mechanism available to an extension depends on your core package version. Treat the contract (fetch, deliver, return the cursor) as fixed and confirm the delivery path with REDA when building a provider.

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

MethodSignatureContract
submitglobal 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
checkStatusglobal abstract EInvoiceResult checkStatus(refin__E_Invoice_Trail__c trail)Polling for asynchronous authorities that answer later
cancelglobal 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 fieldRole
Entity__c · Country_Code__c · Environment__c · Is_Active__cWhich company, which jurisdiction, test vs production, on/off
Provider__cDisplay name only
Provider_Class__cThe fully qualified Apex class extending refin.EInvoiceProvider (the field's own example: sovos.SovosEInvoiceProvider)
Document_Types__cMulti-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__cVerbatim: "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.

Why contracts instead of built-ins? Bank APIs and tax authorities differ per country and change on their own schedules. A stable global contract lets specialist packages evolve at their own pace — and lets your org swap providers by changing a setup record, not re-implementing banking.

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.

InterfaceMethodsImplement it when
refin.EInvoiceQueueProviderdrain(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.EInvoiceDocumentProviderfetchDocuments(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.EInvoiceProvisionerprovisionOrganization(setup, args); provisionCompany(setup, args) — both returning EInvoiceProvisionResultYou 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 after you've persisted. The core calls 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 by erpDocumentId, a value your provider stamps into the outbound envelope and the authority echoes back. status is normalised to Accepted, Rejected, Cancelled or InProgress — the core keys retry and terminal-state logic off that, never off the raw responseCode. It also carries notificationId, processType (Outbound / Inbound), responseMessage, fiscalUUID, fiscalNumber, an optional documents list, payload and eventDate.
  • refin.EInvoiceDocument — one artefact: title, fileExtension ('xml' or 'pdf'), base64Body, and documentKind ('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 to false), externalId (the provider-side handle) and message.

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

MistakeWhy it hurtsDo this instead
Typos in Provider_Class__cRuntime resolution is by name — a wrong string means the provider simply never runsCopy the fully qualified name (namespace.ClassName) from the extension's docs; test the connection immediately
Relying on Provider__c to switch providersIt'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 recordsThe 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 internalsProvider packages evolve on their own schedule — only refin. global contracts are the stable seamIntegrate through the setup records and trails; treat the provider as a black box
Assuming orchestration details without testing your versionScheduling and auto-dispatch behavior is version-dependentVerify 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

  1. Importing Bank Statements — the flow feeds land in.
  2. AppExchange Integrations — the broader ecosystem picture.
  3. 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?