movere Docs

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_method or through the saved-method resolution described below. Without either, the gateway returns 400 server_flow_requires_payment_method before provider execution. If your cardholder is on a browser surface and you don't want to host the Payment Element yourself, use flow: "redirect" or flow: "embedded" instead — Stripe Checkout collects the card for you and you don't pass payment_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:

  1. 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.
  2. The Stripe provider card is highlighted with a brand-coloured ring.
  3. return_url is validated client-side (must be https:// 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 in sessionStorage so it survives the round-trip through Stripe's hosted onboarding.
  4. 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.
  5. Movere pulls fresh status from Stripe (account.updated webhook plus an explicit sync), and once at least one connection upgrades to charges_enabled=true we auto-bounce the merchant back to your return_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_checkouts for 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.