Goal#
This page is the contributor guide for adding a new connector to
formancehq/payments. If your
team uses a payment service provider Formance does not integrate with,
the project welcomes a pull request that wires it in. Merged connectors
ship in the Community edition binary.
The platform side stays generic: the framework schedules the connector's fetchers and routes initiation workflows. The contributor writes the provider-specific glue — auth, HTTP plumbing, format conversion, and metadata mapping.
Scope#
This guide covers CE PSP connectors — Community edition payment service providers that fetch accounts, balances, payments, and (where the upstream supports it) initiate transfers and payouts. Stripe, Bankingcircle, Mangopay, Wise are the kind of integrations to follow.
Out of scope here:
- Enterprise connectors. EE plugins live under
ee/plugins/and ship in a separate binary; the source isn't open and external contributions don't apply. - Open Banking aggregators. Plaid, Tink, and Powens implement an additional surface for end-user link sessions and consent renewals on top of the PSP plugin contract. Their pattern is involved enough to warrant a dedicated guide and is not covered here.
What you'll add to the repo#
A new folder at internal/connectors/plugins/public/<provider>/ with:
- A plugin entry point that registers the connector at process start.
- A
Configstruct describing the install-time fields readers fill in. - A workflow declaration listing the periodic fetch tasks.
- A
[]models.Capabilityslice declaring what the plugin supports. - One Go file per capability (
accounts.go,payments.go,webhooks.go, …) implementing thePSPPluginmethods you opted into. - Table-driven unit tests next to each file.
- A
MAPPING.mdcataloguing the upstream-to-Formance field mapping.
When the PR merges, the connector appears on the
Capabilities matrix on the next
docs build. Upstream regenerates docs/other/connector-capabilities.json
on every commit, and the docs site reads that file at build time.
Where the code lives#
Every connector folder is named after the lowercase provider id
(stripe, bankingcircle, mangopay).
The Plugin interface#
The methods you implement are on PSPPlugin plus a small set of
lifecycle hooks. The full surface is in
internal/models/plugin_psp.go:
Plus the lifecycle methods on the parent Plugin interface:
You only implement what your provider supports. The
basePlugin
type returns ErrNotImplemented for every method by default — embed it
in your struct and override the methods that match the capabilities
you'll declare. The platform reads the []models.Capability slice you
register at startup and only routes calls to the methods you opted
into; methods you didn't override are never invoked.
Anatomy of a connector folder#
Stripe is a good reference for a fully-featured CE PSP — accounts, balances, external accounts, payments, transfer + payout initiation, and webhooks. Its folder:
plugin.go — entry point and registration#
init() runs at process start. The linker pulls every package under
public/ in via a blanket import in
registry/plugins.go,
so adding a new connector folder is enough — no central list to edit.
Install returns the connector's workflow tree — see below.
config.go — typed config and validation#
Define a Config struct with json tags and validate tags
(go-playground validator). The registry reads the tag set to extract
the field list for the OpenAPI spec, so a new field appears in the
public docs on the next regeneration without a docs-side edit.
Re-use sharedconfig.PollingPeriod
for the polling cadence so the floor (MinimumPollingPeriod) stays
uniform across connectors. Pick the same minimum every other connector
uses (20m) unless your provider rate-limits at a coarser cadence.
workflow.go — task tree#
The workflow declares which capabilities run periodically at install time and how they nest:
Nested non-periodic tasks (here fetch_balances) get triggered by
their parent at the same cadence — useful when the parent already
pulled the data the child needs.
capabilities.go — declared capability set#
This slice is the source of truth for the
Capabilities matrix on the docs
site — upstream serialises it into
docs/other/connector-capabilities.json
on every commit, and the docs build reads that file. Declare only what
you'll actually implement.
Implementing a capability#
Each FetchNext* method takes a FromPayload, an opaque State, and
a PageSize, and returns a slice of PSP* objects plus the next
State and a HasMore flag. The platform persists State between
cycles, so the connector keeps no in-memory cursors:
The body lives in accounts.go:
Key invariants:
- The cursor lives in
State. The plugin reads it, the platform persists whatever you return asNewState. Crash mid-cycle and the worker resumes from the last persisted checkpoint. HasMore=truetells the platform to call you again immediately for the next page;HasMore=falseends the cycle and the schedule re-triggers afterPollingPeriod.- Metadata goes under a provider-specific namespace —
com.<provider>.spec/<key>— so downstream consumers can find it without colliding with other connectors. - Raw upstream payloads land on
.Rawso consumers needing fidelity beyond the typed surface can reach for them. Available on everyPSP*resource.
CreateTransfer and CreatePayout follow the same request/response
pattern but synchronously return either a Payment (when the upstream
settles immediately) or a PollingTransferID — in the latter case the
platform schedules PollTransferStatus until a terminal Payment
surfaces.
Webhooks#
If the upstream provider supports webhooks, implement the four-method contract:
CreateWebhooks— POST a webhook config upstream pointing at the platform's ingress URL. Persist the upstream config IDs inCreateWebhooksResponse.Configsso the platform can delete them on uninstall.TrimWebhook— split an upstream batched-events payload into onePSPWebhookper event.VerifyWebhook— verify the upstream signature and return the idempotency key the platform should use to dedupe replays.TranslateWebhook— convert a verified upstream payload into aWebhookResponsecarrying the affectedPSPAccount/PSPPayment/PSPBalance.
Declare both CAPABILITY_CREATE_WEBHOOKS and
CAPABILITY_TRANSLATE_WEBHOOKS in capabilities.go so the platform
schedules the create-webhook task at install and routes inbound
webhooks back to your plugin.
Configuration in the OpenAPI spec#
The registry reads the Config struct's reflection metadata and emits
the field list into
openapi/v3/v3-connectors-config.yaml
during the upstream OpenAPI regeneration. The docs build reads that file
via the payments.connectors.<id>.fields data key, and the per-connector
page renders the table from it. A new field upstream surfaces in the
public docs on the next content build with no docs-side edit.
The validate:"required" tag is what the docs surface as the
Required: yes column. Omit it for optional fields.
Testing#
Each connector ships with table-driven unit tests next to each
capability file (accounts_test.go, payments_test.go, …) using
mockgen-generated mocks of the HTTP client. See
stripe/accounts_test.go
for the pattern.
For end-to-end smoke testing against the real upstream API, point the connector workbench at your plugin and a sandbox key — it boots a single-plugin Temporal worker and runs the install workflow without the full platform stack.
Mapping documentation#
Every connector carries a MAPPING.md next to the code that
catalogues:
- Upstream resource → Formance resource field mapping.
- Status enum collapses (upstream values → Connectivity status).
com.<provider>.spec/metadata key inventory.- Known gaps and not-yet-implemented capabilities.
The mapping doc is the reference both for connector reviewers and for
the docs writer who turns it into the per-connector page at
apps/docs/content/pages/modules/connectivity/connectors/psp/<id>.mdx
in the docs repo.
CE references to read alongside this guide#
The shipped CE connectors are the best living references — pick one with a capability set close to your target:
stripe/— full set: accounts, balances, external accounts, payments, transfer- payout initiation, webhooks.
bankingcircle/— bank-rails connector with bank-account creation, transfers, payouts; mTLS + OAuth2 auth.mangopay/— multi-currency e-wallet provider with the full read + write surface and webhook ingest.wise/— multi-profile multi-currency, quote-then-fund payout flow.atlar/— read-only bank-rails connector (smaller surface, useful as a starting point if your provider is observation-only).
When in doubt, copy the closest match into a new folder, rename, and adapt — the framework is consistent across connectors so the boilerplate ports cleanly.