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.
<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:
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:
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).
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.
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.
// 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.
const stripe = Stripe('rip_pk_EXAMPLE-KEY_00000000abcZ');
stripe.elements().create('cardNumber'); // throws api_error not_supported_by_ripper