---
id: migration/stripe
title: Migrating from Stripe
summary: Swap one script URL and one key; elements.create, mount and confirmCardPayment keep working.
faces: ["public", "agent"]
personalises: ["publishable_key"]
section: front-end
group: Migrate
slug: stripe-compatibility
order: 140
---
# Migrating from Stripe

Swap `https://js.stripe.com/v3/` for `https://js.ripper.dev/stripe/v3/` and `pk_live_…` for your
`rip_pk_…`. Your server swaps its API base and secret key and creates PaymentIntents through the
alias routes; the client secret it hands the page IS a ripper client secret.

```html run id=stripe-tag card=0000
<script src="https://js.ripper.dev/stripe/v3/"></script>
<script>
  var stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
  var elements = stripe.elements();
  var card = elements.create('card');
  card.mount('#card');
  document.getElementById('payment-form').addEventListener('submit', function (e) {
    e.preventDefault();
    stripe.confirmCardPayment(window.clientSecret, { payment_method: { card: card } }).then(function (r) {
      console.log(r.paymentIntent ? r.paymentIntent.status : r.error.code);
    });
  });
</script>
```

The classic card flow, in full:

```js run id=stripe-card-flow card=0000
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
const elements = stripe.elements();
const card = elements.create('card');
await card.mount('#card');
const result = await stripe.confirmCardPayment(clientSecret, { payment_method: { card } });
// result.paymentIntent.status === 'succeeded'
```

The Payment Element flow with `confirmPayment` — `redirect: 'if_required'` only; the compatibility layer never
redirects and ignores `return_url`:

```js run id=stripe-payment-flow card=0000
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
const elements = stripe.elements({ clientSecret });
const payment = elements.create('payment');
await payment.mount('#card');
const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: 'https://example.com/return' }, redirect: 'if_required' });
```

`retrievePaymentIntent` reads the session state; `paymentIntent.status` is mapped from ripper's:
`captured` → `succeeded`, `held` → `requires_capture`, `processing` → `processing`, awaiting a card →
`requires_payment_method`, awaiting authentication → `requires_action`, declined, expired or a voided hold →
`canceled` (`cancellation_reason` `abandoned` for an expired session, `automatic` for a voided hold, otherwise `null`).

```js run id=stripe-retrieve card=0000
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
const card = stripe.elements().create('card');
await card.mount('#card');
await stripe.confirmCardPayment(clientSecret, { payment_method: { card } });
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
console.log(paymentIntent.status); // 'succeeded'
```

## The shopper's email

Every ripper payment needs the shopper's email (the receipt address). The compatibility layer takes it, in order, from
`receipt_email` on the confirm call, then the billing email (`payment_method.billing_details.email` on
`confirmCardPayment`, `payment_method_data.billing_details.email` on `confirmPayment`), then an **Email** field
ripper renders inside your `#card` mount, above the card fields — so an unmodified page still works. A
session your server created with `receipt_email` needs none, and the field does not render when
`elements({ clientSecret })` lets the compatibility layer see that at mount. With no email anywhere, the confirm resolves
`{ error: { type: 'validation_error', code: 'email_required' } }` before any request and focuses the field.

```js run id=stripe-receipt-email card=0000
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
const card = stripe.elements().create('card');
await card.mount('#card');
const result = await stripe.confirmCardPayment(clientSecret, { receipt_email: 'shopper@example.test', payment_method: { card } });
```

A soft decline comes back as `{ error: { type: 'card_error', code: 'card_declined', decline_code } }`
with the element still mounted for retry; no ripper panel is ever painted inside your `#card`.

## Your order reference

Your Stripe integration almost certainly already sends your own order number in `metadata` — `order_id`,
`order_number`, whatever you called it. Name that key once on your ripper integration (you name the KEY, not
the value) and every Stripe-compatible payment carries your reference: it is on the payment read and on the
webhook beside ripper's payment ID, and where ripper's own checkout is shown it appears on the confirmation
as **Order 1042** and travels to your confirmation page.

```js id=stripe-order-reference-server
// your server, unchanged — the key you already send is the key you name
await stripe.paymentIntents.create({ amount: 4200, currency: 'gbp', metadata: { order_id: '1042' } });
```

Nothing on your page changes, and turning it on cannot start failing payments that were working. A value the
order-reference rule cannot take — longer than 64 characters, or carrying a control character — is simply not
taken: the payment goes through exactly as before, your metadata is delivered untouched, and ripper records
why on its own side rather than refusing the payment.

## Not emulated

Thrown as `api_error` with code `not_supported_by_ripper`: the split card elements (`cardNumber`,
`cardExpiry`, `cardCvc`), the `address`, `linkAuthentication`, `expressCheckout` and
`paymentRequestButton` elements, `redirect: 'always'`, `createToken`, `createPaymentMethod`,
`createSource` (no card number ever reaches ripper), `handleCardAction`, `confirmSetup` /
SetupIntents, `redirectToCheckout`, Apple/Google Pay, Stripe's test card numbers (the reserved
cards apply), Stripe webhook signatures (your server verifies ripper's), and any API version header.

```js run id=stripe-not-supported outcome=error:not_supported_by_ripper
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
stripe.elements().create('cardNumber'); // throws api_error not_supported_by_ripper
```
