Skip to content
Elara One docs

Payments

Take card payments through the store's own Stripe account and PayPal, handle test and live mode, and turn declines into clear messages.

Elara One takes payments through the store's own Stripe and PayPal accounts. The money goes straight to the merchant, and card details are entered into Stripe's or PayPal's own form, so they never reach Elara One or your servers. This page covers how the two providers fit into checkout and how to handle a payment that does not go through.

Card data never touches Elara One

  • Stripe. The shopper types their card into Stripe's Payment Element, which runs in an iframe served by Stripe. Your page and the API only ever see a PaymentIntent id (pi_…).
  • PayPal. The shopper pays in PayPal's own buttons and window. The API only sees the PayPal order id.

Your storefront server and the API never handle card numbers.

Connect the providers

Merchants connect their accounts in the store admin under Settings → Payments. Each provider has a set of Test keys and a set of Live keys. The page also shows the webhook address on the store's own hooks host and the provider events to send to it.

Your storefront reads what it needs from GET /v1/checkout/config: which gateways are on, the Stripe publishable key and the PayPal client id. You never handle the merchant's secret keys.

Test and live mode

gateway_mode in GET /v1/checkout/config is test or live, and says which set of keys checkout is using. It is set for the store by Elara One rather than switched in the admin, and the top of the Payments page shows which mode is in use.

  • In test mode, show a "test payments" banner, and use the provider's test cards and sandbox accounts.
  • Load PayPal's JavaScript SDK with the client id for the same mode.
  • Staging stores, where your plan includes them, always use test payments.

Stripe

The Stripe flow uses a PaymentIntent and the Payment Element:

  1. POST /v1/checkout/payment-intent returns a client_secret and the publishable_key for the cart's current total.
  2. Mount the Payment Element with the client secret and let the shopper confirm.
  3. Call POST /v1/checkout/complete with { "payment": { "gateway": "stripe", "payment_intent": "pi_…" } }.
In the browser, with Stripe.js
const stripe = Stripe(publishableKey)
const elements = stripe.elements({ clientSecret })
elements.create('payment').mount('#payment-element')

// On "Pay":
const { error, paymentIntent } = await stripe.confirmPayment({
  elements,
  redirect: 'if_required',
  confirmParams: { return_url: 'https://hearthandoak.example/checkout/return' },
})
if (!error) {
  // Ask your server to call POST /v1/checkout/complete with paymentIntent.id.
}

When the money is taken

stripe_capture_method in GET /v1/checkout/config tells you how the PaymentIntent takes the money:

ValueWhat happens
manualConfirming the payment in the browser only authorises the card (the PaymentIntent ends in requires_capture). checkout/complete checks stock, shipping and promotions, and only then captures. If it refuses, the authorisation is released and nothing is charged.
automaticConfirming the payment takes the money. If the order can then no longer be placed, the payment is refunded and checkout returns 409 PAYMENT_REFUNDED.

If you mount the Payment Element before creating the PaymentIntent (a deferred intent), pass stripe_capture_method as Elements' captureMethod. After any refusal from checkout/complete, call POST /v1/checkout/payment-intent again before letting the shopper pay again.

PayPal

  1. In the PayPal buttons' createOrder, call POST /v1/checkout/paypal/order and return its paypal_order_id.
  2. In onApprove, call POST /v1/checkout/paypal/capture with { "paypal_order_id": "…" } and an Idempotency-Key.

The capture is recorded before the order is placed, so a retry never captures twice. If PayPal refuses the shopper's funding source, nothing is taken: let them choose another in the PayPal buttons and call the capture again for the same PayPal order.

The starter storefronts offer Stripe card checkout. PayPal is available in the API for storefronts you build yourself.

Declined payments

When a payment does not go through, POST /v1/checkout/complete and POST /v1/checkout/paypal/capture answer 402 with error.code PAYMENT_FAILED. When the provider declined it, details.decline_code says why:

402 Payment Required
{
  "error": {
    "code": "PAYMENT_FAILED",
    "message": "PayPal declined the payment method. Choose another way to pay and try again.",
    "details": { "decline_code": "try_another_method" }
  }
}
decline_codeMeaningWhat to show
card_declinedThe card issuer or PayPal refused it without a reason the shopper can act on. Lost, stolen and fraud-filter declines are reported this way on purpose."Your card was declined. Try another card or way to pay."
card_expiredThe card has expired.Ask for another card.
insufficient_fundsNot enough funds or credit.Ask for another card or way to pay.
incorrect_card_detailsA wrong card number, security code (CVC) or postcode.Ask the shopper to check and re-enter their details.
authentication_required3-D Secure (Stripe) or PayPal's own check was not completed.Ask the shopper to complete their bank's check, then try again.
try_another_methodPayPal refused this funding source.Reopen the PayPal buttons so the shopper can pick another, then capture the same PayPal order again.
unknownThe provider declined for a reason Elara One does not map, or gave none.Show error.message.
  • Treat any value you do not recognise as unknown. The list may grow.
  • error.message is Elara One's own wording, safe to show. The provider's raw text is never returned.
  • details is only present on a provider decline. Other refusals, such as a released authorisation or a payment made for another store, have only code and message: show the message.
  • After a 402, the cart and stock are unchanged and the shopper can try again. Branch on error.code first: a 402 with TENANT_PAYMENT_REQUIRED means the store itself is unavailable, not that the payment failed.
Handle a checkout response
const body = await res.json()

if (res.status === 402 && body.error.code === 'PAYMENT_FAILED') {
  const code = body.error.details?.decline_code ?? 'unknown'
  if (code === 'try_another_method') reopenPayPalButtons()
  showPaymentError(body.error.message)
}

The other checkout outcomes, such as SHIPPING_OPTION_CHANGED and PAYMENT_REFUNDED, are covered in checkout.