Connect provider implementation notes
Back to the Movere Connect protocol.
These notes describe existing provider-specific implementation contracts. They are not provider recommendations or a statement that these features are supported by every provider. Use only the notes applicable to the merchant's enabled connection and request environment.
Server confirmation
The following token and saved-method examples apply to the Stripe adapter.
flow: "" (default) / "server" — Server-to-server confirm
Use this when you already have a tokenised payment method on hand
(a pm_xxx you collected previously via a Setup Intent, or the
cardholder's confirmation_token / ctoken_xxx from a Payment
Element you host inline). The gateway confirms the PaymentIntent
and returns its resulting state. Additional customer action or provider
processing may still be required; do not assume every response is terminal.
Supply a tokenized payment method, either in
payment_methodor through the saved-method resolution described below. Without either, the gateway returns400 server_flow_requires_payment_methodbefore provider execution. If your cardholder is on a browser surface and you don't want to host the Payment Element yourself, useflow: "redirect"orflow: "embedded"instead — Stripe Checkout collects the card for you and you don't passpayment_method.
POST /v1/payment_intents?environment=test HTTP/1.1
Host: api.mooov.money
Content-Type: application/json
X-Mooov-Key-Id: mk_platform_<id>
X-Mooov-Timestamp: 2026-05-21T08:39:00Z
X-Mooov-Signature: 1a2b3c4d… (lowercase hex HMAC-SHA256)
Mooov-Merchant: merch_lodge_001
Idempotency-Key: c5b0…
{
"payment_id": "pay_…",
"amount": 4200,
"currency": "GBP",
"payment_method": "pm_example"
}
For Live requests (?environment=live), you may also drive a
saved-method charge with customer_ref + setup_future_usage: "off_session"
instead of payment_method. The gateway resolves the saved PaymentMethod
on the connected account's Stripe Customer record. Customer-reference
and saved-method workflows are not available in Test; use an explicit
provider test payment-method token in the Test server-flow example.
If the tenant has not yet finished PSP onboarding you get the typed 422 documented in the onboarding error reference; otherwise the response is the standard PaymentIntent envelope.
Connection recovery
5.2 Reconnect deep-link (Stripe account recovery)
If a tenant's underlying PSP connection (today: Stripe Connect)
breaks — most commonly because Stripe revoked OAuth, the connected
account hit a hard requirement past_due, or the lodge admin clicked
Disconnect inside their Stripe dashboard, a Live payment failure
may carry failure_code: "account_invalid" or a related Stripe code.
A charge can also be rejected before provider execution; handle the
API error as well as subscribed Live payment events.
Reconnecting via the Movere OAuth consent flow in section 3 of the Connect protocol does not fix this — the Movere grant is still active, the problem is one layer down. The fix is to send the lodge admin to the Movere merchant portal's Stripe-reconnect surface:
https://mooov3.mooov.money/connections
?from=<your-platform-slug>
&action=stripe_reconnect
&return_url=<your https URL — urlencoded>
Example (LodgePay):
https://mooov3.mooov.money/connections
?from=lodgepay
&action=stripe_reconnect
&return_url=https%3A%2F%2Fwww.lodgepayments.co.uk%2Fadmin%2Fintegrations
What happens on the Movere side:
- The page shows a branded banner ("
<Your platform>sent you here to repair your Stripe connection") so the lodge admin knows where they came from and where they're going back to. - The Stripe provider card is highlighted with a brand-coloured ring.
return_urlis validated client-side (must behttps://and resolve to an origin we explicitly recognise — today:lodgepayments.co.uk,www.lodgepayments.co.uk,*.vercel.app; new integrators are added on request) and stashed insessionStorageso it survives the round-trip through Stripe's hosted onboarding.- The lodge admin clicks Continue setup, completes whatever
Stripe needs (requirements, re-authorisation, bank account
re-verification, etc.), and lands back on
/connections?from=psp_return. - Movere pulls fresh status from Stripe (
account.updatedwebhook plus an explicit sync), and once at least one connection upgrades tocharges_enabled=truewe auto-bounce the merchant back to yourreturn_url. If nothing upgrades (Stripe still rejecting) the merchant stays on the Movere page with the actual error visible so they can try again.
The return_url allowlist exists specifically to prevent
mooov3.mooov.money/connections?from=...&return_url=https://evil.example.com
being used as an open-redirect from a logged-in merchant session.
Want a new origin added? Email ops@movere.money with the
production + preview origins you need; we add them inline with
your integrator config and ship a tiny merchant-app change.
Integrator recovery example when account_invalid fires
// In your payment.failed webhook handler:
if (event.data.failure_code === "account_invalid") {
await markMerchantAsNeedsReconnect(event.merchant.id);
// In the merchant's UI, replace the "Process payment" CTA with:
const url = new URL("https://mooov3.mooov.money/connections");
url.searchParams.set("from", "lodgepay");
url.searchParams.set("action", "stripe_reconnect");
url.searchParams.set("return_url", `${YOUR_ORIGIN}/admin/integrations?reconnect=ok`);
showReconnectCTA({ href: url.toString() });
}
When the merchant returns to your origin via return_url, treat
that as provisional confirmation only. account.updated is a
provider-to-Movere event, not a public Connect event. Verify the next
successful charge before flipping your tenant flag back to "connected". The deep-link only
bounces back when our side saw charges_enabled=true, but a
belt-and-braces re-check on your side is the right contract.
Subscription events
The following subscription projection currently describes Stripe-backed subscriptions. Do not apply its provider-specific fields to other adapters.
For eligible Live platform integrations, create a hosted subscription with
POST /v1/subscription_checkouts; see the request fields and current Test
restriction in the API reference. Integrations with
the necessary direct Stripe access can also create subscriptions against
the connected account using Stripe's API and idempotency controls.
Movere fans out lifecycle events for supported subscriptions on the
connected account. It projects
customer.subscription.* / invoice.* webhook into a stable
subscription_id namespaced under the merchant. The five event
types below are the integrator-facing names; rename or removal is a
breaking Connect change covered by section 10 of the Connect protocol.
type |
When it fires (Stripe trigger) |
|---|---|
subscription.activated |
First customer.subscription.created on the connected account. The projection upserts a row and emits this event once per subscription, idempotent on stripe_event_id. |
subscription.updated |
Any customer.subscription.updated that is NOT a status transition to canceled / incomplete_expired (we collapse those into subscription.canceled below). |
subscription.canceled |
Either customer.subscription.deleted or a customer.subscription.updated whose new status is canceled / incomplete_expired. The two cases dedupe on event.id. |
subscription.invoice_paid |
invoice.paid for a subscription cycle. Dual-emitted alongside payment.succeeded for the same cycle — see the dual-emit note below. |
subscription.invoice_failed |
invoice.payment_failed for a subscription cycle. Dual-emitted alongside payment.failed. After Stripe's Smart Retry chain gives up the same envelope fires again. |
Dual-emit contract. Every successful subscription cycle produces
two webhook deliveries with two distinct event.ids: one
subscription.invoice_paid (subscription lane, carries the
projection's subscription_id and the Stripe invoice id) and one
payment.succeeded (payment lane, carries a per-cycle payment_id
the integrator can refund via POST /v1/payment_intents/{id}/refund).
The same pair fires on failure as subscription.invoice_failed +
payment.failed. Integrators who already process payment.* events
need no code change for the payment lane; the subscription lane is
the new surface and is opt-in via your webhook destination's
event-type allowlist in the operator console.
Per-cycle event payload (subscription.invoice_paid / subscription.invoice_failed).
The data envelope carries the Stripe invoice id, the resolved
subscription_id, period markers, and a verbatim copy of the
subscription's metadata bag (the bag you set on the Stripe
Subscription itself, NOT the per-invoice metadata). LP-side cycle
correlation reads data.subscription_metadata.lp_schedule_id and
data.subscription_metadata.cycle_number; populate those on your
side when you create the Subscription.
{
"id": "evt_stripe_subscription_subscription.invoice_paid_<stripe_event_id>",
"type": "subscription.invoice_paid",
"created": "2026-06-01T00:00:01.000Z",
"merchant": { "id": "merch_lodge_001", "entity_id": "mooov3" },
"data": {
"subscription_id": "sub_merch_lodge_001_abc123",
"stripe_subscription_id": "sub_abc123",
"stripe_customer_id": "cus_xyz789",
"customer_ref": "mbr_22222222-2222-4222-8222-222222222222",
"subscription_status": "active",
"amount": 1250,
"currency": "GBP",
"interval": "month",
"interval_count": 1,
"period_start": "2026-06-01T00:00:00Z",
"period_end": "2026-07-01T00:00:00Z",
"source_event_id": "evt_<stripe_event_id>",
"subscription_metadata": {
"lp_schedule_id": "sched_lp_test_1",
"lp_member_dues_id": "mdues_lp_test_1"
}
}
}
Fields are omitted when empty rather than emitted as JSON null,
so a schema validator that rejects unexpected nulls (Ajv with
strict: true, Pydantic v2 strict mode) still passes. Specifically
customer_ref, stripe_customer_id, period_start, period_end,
cancel_at, canceled_at, and subscription_metadata are dropped
entirely when not present on the projection. Always check
if "customer_ref" in data before reading.
Payload precision. The top-level created timestamp has exactly
three fractional digits. Subscription period_start, period_end,
cancel_at, and canceled_at use RFC3339 UTC with whole seconds, as
shown in the example; do not require fractional digits on those fields.
Cancellation events. subscription.canceled is dual-sourced:
Stripe's customer.subscription.deleted fires when an integrator
calls DELETE /v1/subscriptions/<id> immediately, and
customer.subscription.updated with status="canceled" fires when
the cancellation was scheduled via cancel_at. Both flow through
the same projection path so the integrator only ever sees
subscription.canceled. The original-sub-row carries canceled_at
in data so you can branch on "scheduled" vs "immediate" on your
side if you need to.
Migrating from scheduled saved charges. Use
POST /v1/subscription_checkoutsfor the supported hosted subscription setup, then handle activation and invoice events. Plan the cutover so a scheduled saved charge and a subscription invoice cannot charge the same billing period. Custom billing anchors and cancellation schedules require appropriate direct Stripe access; they are not fields on Movere's hosted subscription endpoint. Existing payment handlers must deduplicate the payment/invoice event pair described above.