Engineering May 2026 12 min read · 3,000 words

Stripe Connect + Multi-Card Splitting:
How the Quarvo Payment Graph Works

A walkthrough of the architecture behind splitting a single $2,400 customer transaction across 2–4 cards atomically, settling in full to one merchant, and shipping as a Stripe Connect platform — without the merchant changing processors. Real code, real failure modes, real timing.

The shape of the problem

A customer hits checkout with a $2,400 cart. Their best card has $1,800 of available credit. The straight charge fails. Their second card has $1,400 available. Their combined credit is $3,200 — enough — but no checkout in commerce is built to use both cards on one transaction.

The technical reason isn't capability. Card networks have supported partial authorizations for decades. The reason is coordination: there is no consumer-facing layer that schedules N partial authorizations against N cards, holds them simultaneously, and commits or rolls back atomically. That coordination layer is what we call the payment graph.

Quarvo is the payment graph. The rest of this post is how it's built.

Why Stripe Connect is the right substrate

The first design decision is what runs underneath the graph. Three options exist for a payment-orchestration layer:

Stripe Connect was built for marketplaces where a platform processes payments on behalf of sellers (Lyft, DoorDash, Substack). The pattern fits split-card recovery exactly: Quarvo is a "platform" whose value is the orchestration of multiple authorizations on behalf of a merchant. The merchant sees a single PaymentIntent on their dashboard and a single net deposit.

// CONNECT MODEL CHOICE

Quarvo runs as a Standard Connect platform with destination charges and an explicit application_fee_amount. Merchants keep ownership of their Stripe account, dispute responsibility, and settlement timing. Quarvo never custodies funds. Compliance-wise, this puts Quarvo in the same category as e-commerce SaaS that uses Stripe billing — not in the money-transmission category.

Architecture in one diagram

Quarvo payment graph — request flow
// SINGLE TRANSACTION · N CARDS · ATOMIC COMMIT
// 01
Merchant checkout
creates session
// 02
Quarvo coordinator
/api/checkout/session
// 03
Customer card vault
tokenized · PCI-DSS
// 04 · TWO-PHASE COMMIT
PaymentIntent fan-out — N parallel auths in requires_capture
phase 1: prepare · ~600–1100ms · all-or-nothing
// 05a
Stripe (Card 1)
$1,800 captured
// 05b
Stripe (Card 2)
$600 captured
// 05c
Stripe (Card N)
parallel capture
// 06 · SETTLEMENT
Merchant Stripe account
net $2,400 − application_fee_amount → merchant payout schedule

Six conceptual nodes, three architectural layers: a thin coordinator (Quarvo's API), a vault (PCI-scoped customer card storage), and the merchant's existing Stripe Connect account where authorizations and capture happen. The coordinator orchestrates; Stripe processes; the merchant receives one consolidated settlement.

The two-phase commit, in detail

The hardest engineering problem in a multi-card transaction is partial failure. If you authorize Card A successfully and then Card B fails, the customer's available credit on Card A is still being held — they will be confused, and the merchant has nothing to capture against. The naive sequential approach is unsafe.

Quarvo uses a two-phase commit pattern, the same primitive that distributed databases use to ensure atomicity across multiple resource managers. Phase 1 prepares all authorizations. Phase 2 commits all of them, or none.

Phase 1 — Prepare

All N authorizations are launched in parallel as PaymentIntents in requires_capture state with capture_method: "manual". This holds the funds without charging the customer. The system waits for all N to either succeed or fail.

// NODE.JS phase1.prepare.js
// Phase 1: launch N parallel authorizations on the merchant's Stripe account
const authorizations = await Promise.allSettled(
  splits.map((split) =>
    stripe.paymentIntents.create(
      {
        amount: split.amount,                // in cents
        currency: 'usd',
        payment_method: split.cardToken,
        confirm: true,
        capture_method: 'manual',    // HOLD, don't capture
        application_fee_amount: computeFee(split.amount),
        metadata: {
          quarvo_session_id: session.id,
          split_index: split.index,
          split_total: splits.length,
        },
      },
      { stripeAccount: merchant.stripeAccountId }
    )
  )
);

const failures = authorizations.filter(a => a.status === 'rejected' || a.value.status !== 'requires_capture');
if (failures.length > 0) {
  await rollbackAll(authorizations);
  throw new SplitFailedError(failures);
}

Two key properties: Promise.allSettled rather than Promise.all (we want partial success info, not early termination), and capture_method: 'manual' on every intent (so we can void cleanly if anything fails). The window during which authorizations are held is bounded by Stripe's auth-hold window (typically 7 days for most issuers, but Quarvo enforces a much shorter internal window — see below).

Phase 2 — Commit

If all N authorizations are in requires_capture state, the commit phase fires N captures in parallel. Each capture finalizes its respective authorization. The merchant's Stripe account ends up with N successful charges, all linked to the same Quarvo session via metadata.

// NODE.JS phase2.commit.js
// Phase 2: capture all authorizations in parallel
const captures = await Promise.allSettled(
  authorizations.map(a =>
    stripe.paymentIntents.capture(a.value.id, {}, { stripeAccount: merchant.stripeAccountId })
  )
);

const capFailures = captures.filter(c => c.status === 'rejected');
if (capFailures.length > 0) {
  // rare — capture after successful auth almost never fails
  // when it does, refund any successful captures and surface the error
  await refundCapturedSplits(captures);
  throw new CaptureFailedError(capFailures);
}

return { sessionId: session.id, status: 'succeeded', captures };

The rollback path

If Phase 1 returns any failure, we cancel every successful authorization immediately. This is the most important code path in the system — partial holds are the worst possible state from a customer-trust perspective.

// NODE.JS rollback.js
async function rollbackAll(authorizations) {
  // cancel all successful holds; failed ones don't need cancellation
  const toCancel = authorizations
    .filter(a => a.status === 'fulfilled' && a.value.status === 'requires_capture');

  await Promise.allSettled(
    toCancel.map(a =>
      stripe.paymentIntents.cancel(a.value.id, {}, { stripeAccount: a.value.transfer_data.destination })
    )
  );

  // available credit on each card is restored within minutes
  // customer is shown which card failed and offered to swap
}
// CRITICAL INVARIANT

The customer must never see a partial charge. If Phase 1 fails on any card, every successful authorization on every other card must be voided before the customer is shown the error. Quarvo's coordinator runs a strict 5-second timeout on Phase 1; if any card hasn't responded by then, it counts as a failure and triggers the rollback path.

Timing characteristics

End-to-end transaction timing
// HAPPY PATH · 2 CARDS · MEDIAN VALUES FROM PRE-LAUNCH PILOTS
t=0ms
Customer confirms splitDrop-in widget posts to /api/checkout/session with split intent.
+45ms
Coordinator validates sessionCards still mapped to vault, amounts add to total, merchant account active. Issues N parallel paymentIntents.create calls.
+650ms
All N authorizations returnMedian Stripe + issuer round-trip is ~600ms per card; running them in parallel keeps the wall-clock at ~650ms regardless of N (up to ~6 cards).
+750ms
Phase 2 captures fireN parallel paymentIntents.capture calls. Captures are fast — typically ~100ms each.
+860ms
All captures confirmedCoordinator emits checkout.session.completed webhook to merchant. UI shows success.
+1.0s
Customer sees confirmationTotal wall-clock for the customer: under 1 second from confirmation tap to success state, indistinguishable from a single-card checkout.

Parallel execution is what makes this feel like a single-card checkout. The temptation in early designs is to authorize cards sequentially — "if Card 1 succeeds, then try Card 2" — but that adds a full round-trip per card and degrades the customer experience. Parallel + atomic commit is the only design that holds latency flat as N grows.

The reconciliation layer

Each split shows up on the merchant's Stripe dashboard as a separate charge — same customer name, same order ID via metadata, but distinct PaymentIntents. This is correct behavior (each card is a separate authorization), but it creates a reconciliation problem: the merchant's order management system expects one charge per order.

Quarvo emits a single checkout.session.completed webhook with all N charges grouped under one session. The merchant's webhook handler treats the session as one logical order and writes one row to its OMS. The N underlying Stripe charges are linked via the quarvo_session_id metadata field.

Session-level identifier cs_qrv_xxx
Stripe charges per session N (1–6)
Merchant settlement rows 1 (consolidated)
Quarvo fee model application_fee_amount
Dispute responsibility Merchant's account
Refund flow Atomic, all-card

Refunds are also atomic. If a customer requests a refund post-fulfillment, the merchant calls POST /api/checkout/session/:id/refund with the amount. Quarvo distributes the refund proportionally across the N original cards — Card 1 receives 75% of the refund, Card 2 receives 25%, matching the original split — and emits a single checkout.session.refunded webhook. The merchant never has to think about which card to refund.

Read the full integration docs.

End-to-end API reference, drop-in JS snippets, server SDK examples, webhook signatures, and brand kit. Ships as a Stripe Connect platform — no replatforming required.

Open the integration guide →

// PILOT · Q2 2026 · STRIPE · ADYEN AND BRAINTREE TO FOLLOW

What "graph" means in payments

The word graph is doing real work in the name "payment graph." It's not branding — it describes the data structure underneath the orchestration.

A traditional payment is a tuple: (customer_card, merchant_account, amount). A linear, single-edge relationship. There's nothing to coordinate.

A Quarvo payment is a graph. Nodes: a customer, N customer cards, one merchant. Edges: N partial payments connecting customer cards to the merchant, plus one logical "session" node tying them together. Every operation — authorize, capture, refund, void, dispute — has to traverse the graph atomically.

The coordinator's job is to maintain this graph as a consistent unit. The cards don't know about each other. The merchant doesn't see N transactions; it sees one session. The customer doesn't manage allocation themselves; the system does. The graph is what abstracts away the multi-party complexity.

Most payment infrastructure assumes one customer, one card, one merchant. Quarvo assumes one customer, N cards, one merchant — and treats the relationship as a graph the platform owns. That's the only architectural change. Everything else is conventional Stripe.

Edge cases the system has to handle

Three failure modes are interesting enough to call out:

Card-on-file expiration mid-session

If a customer's vaulted card expires between when the session is created and when Phase 1 fires, that card's PaymentIntent will fail. Quarvo's coordinator catches the specific Stripe error code (expired_card), prompts the customer to update the card in-flow, and retries that single split without affecting the others.

Issuer step-up authentication (3DS)

Some issuers will require 3DS challenge mid-authorization. When that happens on one of N cards in a split, Quarvo's coordinator pauses Phase 1 — the other N-1 authorizations have already completed and are held. The customer is shown the 3DS challenge; once they pass, Phase 1 resolves and Phase 2 fires normally. If they cancel or fail, we roll back.

Network partition between Phase 1 and Phase 2

The least likely but most operationally important failure: all N Phase 1 auths succeed, then the coordinator loses connectivity before Phase 2 fires. Quarvo's coordinator persists Phase 1 state to a transactional queue; on recovery, it resumes from the persisted state and either commits or rolls back. The Stripe-side authorizations remain valid for ~7 days (the issuer-side hold window), giving operations far more headroom than they need.

// FREQUENTLY ASKED QUESTIONS
How does multi-card splitting work technically?
It executes N concurrent partial authorizations — one per customer card — and treats them as a single atomic unit. If all N succeed within the auth window, all are captured and the merchant receives one consolidated settlement. If any single auth fails or times out, all prior auths are immediately voided. The customer is never charged unless the full transaction completes. Quarvo coordinates this through a payment graph layer running on top of Stripe Connect's platform model.
Why use Stripe Connect for split-card payments?
Stripe Connect's platform model lets a service like Quarvo orchestrate payments on behalf of merchants without holding merchant funds, becoming a money transmitter, or making merchants change processors. The merchant keeps their existing Stripe account, settlement timing, and dispute infrastructure. Quarvo runs as a Connect platform that creates PaymentIntents on the merchant's account, with Quarvo's fee deducted via application_fee_amount.
How does atomic commitment work across multiple authorizations?
Quarvo uses a two-phase commit. Phase 1 (prepare): all card authorizations are created in parallel as PaymentIntents in requires_capture state. Phase 2 (commit): once all authorizations succeed, all are captured in parallel. If any authorization fails or times out during Phase 1, all successful authorizations are immediately voided via the Stripe cancel API. The merchant only ever sees a successful capture or no charge at all.
What happens if one card fails after others have authorized?
The coordinator detects the failure and immediately voids all previously successful authorizations on the other cards. Available credit on those cards is restored within minutes (issuer-dependent). The customer is shown an inline message describing which card failed, and they can swap in a different card to retry. The merchant is not charged a fee for failed splits. No partial captures ever post.
Does the merchant need to change their payment processor?
No. Quarvo runs as a Stripe Connect platform on top of the merchant's existing Stripe account. The merchant keeps their current account, settlement timing, chargeback handling, and banking arrangements. Integration is a JavaScript drop-in (Shopify/Woo) or a server-side API call (custom). For non-Stripe merchants, Quarvo will support Adyen and Braintree as additional platform substrates in subsequent releases.
M
Marcelo
Founder, Quarvo · Building Credit Combination Infrastructure