# Payment processors

> Connect Stripe or Mollie to a legal entity. Ownrecord stores the keys encrypted, receives the processor's webhooks, charges the totals its own tax quotes compute, and fails over to a brand's secondary processor when the primary has problems.

A **processor connection** links one of your legal entity's accounts at a payment processor to Ownrecord, in one mode. Checkout uses it to take payments, and the processor's webhooks tell Ownrecord when a payment succeeded, failed, was refunded or disputed. Ownrecord, not the processor, works out the tax: the amounts it sends are the final totals of its own quote, so Stripe Tax isn't used.

Ownrecord works with **Stripe** and **Mollie**. Connections belong to the legal entity, like the ledger and registrations. A brand uses its entity's connections: its **primary** connection, or its **secondary** while the primary is degraded (see [Routing and failover](#routing-and-failover)). A brand with no connection assigned uses its entity's active connection in that mode, when there is exactly one. Test mode and live mode have their own connections, with test keys and live keys.

## Connecting Stripe

In the console, open the entity, then **Processor connections**, and choose **Connect Stripe**. You need two keys from the Stripe dashboard (Developers → API keys), from the same account and mode:

- a **restricted key** (`rk_live_…` or `rk_test_…`). Create it with these permissions:

  | Stripe resource                                                                               | Access |
  | --------------------------------------------------------------------------------------------- | ------ |
  | PaymentIntents, SetupIntents, PaymentMethods, Customers, Refunds, Disputes, Webhook Endpoints | Write  |
  | Balance, Payouts                                                                              | Read   |

  In test mode the account's secret test key (`sk_test_…`) also works, for development. Live mode needs a restricted key, so Ownrecord can do only what it needs on your account.

- the **publishable key** (`pk_live_…` or `pk_test_…`), which the payment form in checkout uses.

Ownrecord tests the key before storing anything. A key Stripe rejects, a key of the other mode or a restricted key missing permissions is refused, and the error lists the missing permissions so you can edit the key and try again. Then Ownrecord creates a webhook endpoint on your Stripe account, at `https://ownrecord.io/api/webhooks/stripe/{connectionId}`, subscribed to the payment, refund, dispute and payout events it handles, and pinned to the Stripe API version it's built for.

The same Stripe account can be connected to an entity only once per mode. To use a new key, **rotate** it on the existing connection: the new key is tested and must belong to the same account.

### On a development machine

Stripe can only send webhooks to a public `https` address. Locally, use the Stripe CLI instead of letting Ownrecord create the endpoint:

1. run `stripe listen --print-secret` and copy the signing secret (`whsec_…`; it stays the same between runs);
2. connect with that secret in **Webhook signing secret** (the API's `webhookSecret`);
3. forward events with `stripe listen --forward-to` the connection's webhook URL, shown on the connection.

## Connecting Mollie

Mollie is an EU payment processor with local methods such as iDEAL, Bancontact and SEPA Direct Debit. In the console, open the entity, then **Processor connections**, and choose **Connect Mollie**. Use one of these from the Mollie dashboard:

- an **API key** of the console's mode (`test_…` or `live_…`, under Developers → API keys). It belongs to one website profile, and it does everything checkout, renewals, refunds and chargebacks need.
- an **organization access token** (`access_…`) with the **profile ID** (`pfl_…`) of the website it takes payments for. Reconciling Mollie's balance transactions (fees and settlements) needs a token: Mollie doesn't let API keys read balances. Give it the permissions `payments.read`, `payments.write`, `refunds.read`, `refunds.write`, `customers.read`, `customers.write`, `mandates.read`, `mandates.write`, `profiles.read` and `balances.read`.

Ownrecord tests the key before storing anything: a key Mollie rejects, or an API key of the other mode, is refused. With an API key the connection test passes and reports `balances.read` as missing, as a reminder that reconciliation won't read Mollie's balance until you rotate to a token. A profile can be connected to an entity once per mode.

Mollie needs no webhook setup: every payment Ownrecord creates carries the connection's webhook URL, `https://ownrecord.io/api/webhooks/mollie/{connectionId}`, with a secret token Ownrecord generated for the connection. See [Mollie as your processor or failover](https://ownrecord.io/docs/guides/connecting-mollie.md) for the steps end to end.

### How paying with Mollie works

Checkout with Mollie is a **redirect**: after the customer has entered their details and accepted the terms, Ownrecord creates the Mollie payment for the quoted total and sends the customer to Mollie's payment page, which offers the methods enabled on your Mollie profile. Mollie sends them back to the checkout's success page, which waits until Mollie's webhook has confirmed the payment. A payment that fails, is cancelled or expires on Mollie's page brings the checkout back so the customer can try again; each attempt is a new Mollie payment. Hosted invoice pages pay the same way.

Because the method is chosen on Mollie's page, the card country can't change the tax before paying: the sale is invoiced at the quote the customer paid for.

Subscriptions save a **mandate** at Mollie instead of a card: the first payment of a subscription creates it (an iDEAL or Bancontact payment leaves a SEPA Direct Debit mandate, a card payment a card mandate), and renewals are charged on it without the customer. A trial saves a card with a payment of 0.00. SEPA Direct Debit renewals take a few days to settle; the invoice is marked paid when Mollie confirms.

Mollie charges chargebacks back at once and has no API to submit dispute evidence: the dispute is recorded with its chargeback, and you respond in the Mollie dashboard (the evidence pack PDF is still built for you). A chargeback Mollie reverses closes the dispute as won.

## Routing and failover

Each brand names a **primary** connection and, on plans with the secondary processor feature, a **secondary** one (entity → **Processor connections** → **Brand routing**, or `PATCH /api/v1/processor_routing/{brandId}`). Both are connections of the brand's entity; several brands may share one. The page, and `GET /api/v1/processor_routing`, shows which connection takes each brand's new checkouts now, and why.

A connection is **degraded** when you mark it so, or automatically: when requests to the processor fail several times in a row because it doesn't answer (network failures, timeouts, rate limits, server errors; 5 in a row unless your platform operator set another number), Ownrecord marks the connection degraded, records why, and alerts the organization's owners and admins (`PROCESSOR_DEGRADED`). Declines and refused requests are answers, not failures. While a brand's primary is degraded:

- new checkout sessions use the brand's secondary connection;
- sessions that already started keep their connection, and so do subscriptions: saved payment methods belong to their processor, so a subscription keeps charging the processor of its method until the customer changes it;
- a degraded primary still serves when the brand has no usable secondary.

Ownrecord never marks a connection active again by itself. When the processor works again, choose **Mark active**: the failure count starts over and the alert is resolved. The connection's page shows how many requests failed in a row.

## How keys are kept

Keys and webhook signing secrets are encrypted before they're stored, with a key that belongs to your organization, which is itself encrypted with Ownrecord's master key. They're decrypted only for the moment a call to the processor needs them, and they're never shown again, returned by the API, written to logs or included in events. The console shows a hint such as `rk_test_…AbCd`.

Connecting, rotating and disconnecting are security actions: in the console they need a sign-in within the last 15 minutes, and organization owners and admins can do them. Other roles can see connections and their status.

## Webhooks

Every event the processor sends is checked before anything else happens:

1. The signature is verified with the connection's signing secret. A delivery with a bad signature, or a test event sent to a live connection, is refused.
2. The event is stored once. If the processor sends it again, Ownrecord answers that it already has it.
3. It's handled in the background, usually within seconds: a successful payment completes the checkout, a refund or a dispute updates the payment.

Mollie's payment webhooks carry no signature and say only which payment changed. Ownrecord checks the secret token in the URL instead, then reads the payment from Mollie with its refunds and chargebacks, and stores one event per change it finds (`payment.paid`, `refund.refunded`, `chargeback.received`, …), each once. If Mollie can't be reached, Ownrecord answers so that Mollie tries again later.

The connection shows when the last event arrived, and the list of recent events shows which were processed and which failed, with the error. Handlers never act twice on the same payment, refund or dispute, even when an event arrives twice.

## Testing and status

**Test connection** checks the stored key again and shows which permissions it lacks, if any. The go-live checklist asks for a live connection that passes this test.

If the processor has problems, mark the connection **degraded**: new checkout sessions of brands that use it as primary move to their secondary connection. Mark it **active** again when the problem is over. Ownrecord also marks a connection degraded by itself after repeated failed requests ([Routing and failover](#routing-and-failover)).

**Disconnect** destroys the stored key, so nothing can be charged or refunded through the connection. Assign brands that use it to another connection first. Events Stripe still sends for earlier payments (refunds, disputes) are recorded; delete the webhook endpoint in the Stripe dashboard if you no longer want them. A disconnected Mollie connection's webhooks are acknowledged, but its payments can no longer be read.

## The fake processor

In development and in automated tests, where development access is enabled, you can connect the **fake processor** instead of Stripe. It needs no account and no network, behaves the same every time and sends its webhooks through the same path as Stripe's. It's never available in production.

For Mollie, development and automated tests use a local copy of Mollie's API instead of a Mollie account (with development access and `MOLLIE_API_FAKE=true`; the end-to-end test server sets it): connect Mollie with any test key (`test_` and at least 20 letters or digits), and its payment page is a development page where you choose the method and whether the payment is paid, failed, cancelled or expired. To try a real Mollie test account locally, leave `MOLLIE_API_FAKE` unset and run Ownrecord behind a public `https` address (`AUTH_URL`), so Mollie can reach its webhook.

Its test payment methods decide what happens when a payment is confirmed:

| Payment method                                                      | Result                                                                                    |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `pm_fake_visa`                                                      | succeeds (a US card)                                                                      |
| `pm_fake_card_de`, `…_fr`, …                                        | succeeds, with that card country (for the card-country check)                             |
| `pm_fake_decline`                                                   | declined (`generic_decline`)                                                              |
| `pm_fake_insufficient_funds`                                        | declined (`insufficient_funds`)                                                           |
| `pm_fake_3ds`                                                       | asks for 3-D Secure; confirming again succeeds                                            |
| `pm_fake_3ds_fail`                                                  | asks for 3-D Secure; confirming again is declined                                         |
| `pm_fake_dispute`                                                   | succeeds, then a chargeback (the money and the fee are taken); submitted evidence wins it |
| `pm_fake_dispute_lost`                                              | succeeds, then a dispute is opened; submitted evidence loses it                           |
| `pm_fake_dispute_inquiry`                                           | succeeds, then an inquiry (no money taken); submitted evidence closes it                  |
| `pm_fake_dispute_escalates`                                         | succeeds, then an inquiry that escalates at once to a chargeback; evidence loses it       |
| `pm_fake_dispute_de`, `…_lost_de`, `…_inquiry_de`, `…_escalates_de` | the dispute cards with that card country                                                  |
| `pm_fake_expiring`                                                  | succeeds; the card expires at the end of next month                                       |
| `pm_fake_decline_after_attach`                                      | can be saved for later payments; every payment is declined (`generic_decline`)            |

Saving a card for later payments (a subscription's card, without a payment) is declined when a payment with it would be, except for `pm_fake_decline_after_attach`: use it to test a renewal whose charge fails, and the dunning that follows.

Amounts whose last four minor-unit digits are `9902` are declined, `9903` ask for 3-D Secure and `9904` stay processing (for example EUR 99.02 is declined). A payment intent's metadata `fake_outcome` (`succeed`, `decline`, `require_action`, `processing`) overrides everything else.
