> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withgale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe Payment Elements

> Add Pay with FSA/HSA to a Stripe Payment Element for a one-time charge. Gale hosts the card page and writes the payment back with your restricted key.

Use this page for a one-time charge. The shopper picks **Pay with FSA/HSA**, your server creates a Gale checkout, and Gale hosts the card page. Gale writes the payment record to your Stripe account with the restricted key from Step 2. This option does not create a Stripe PaymentIntent or Charge. Card payments on the same page can keep theirs.

## Prerequisites

* An existing checkout that mounts Stripe's Payment Element.
* Stripe Dashboard access, with permission to create a custom payment method.
* Gale Merchant Dashboard access, and a Gale API key (`glm_test_…` / `glm_live_…`).

Card payments on the same page can keep their own PaymentIntent. The Gale option does not create one.

## Step 1: Create your custom payment method

A **custom payment method type** is what makes Gale appear in your Payment Element. Create one per Stripe account, in each mode.

**1. Open Custom payment methods**

In the Stripe Dashboard, go to **Settings → Payments → Custom payment
methods** and click **Create**.

![Payments settings tabs with Custom payment methods highlighted](https://mintcdn.com/galepaymentsinc/fp_nidsvrE1p2bEe/images/stripe-cpm-3-cpm-tab.png?fit=max\&auto=format\&n=fp_nidsvrE1p2bEe\&q=85\&s=444075bfdd8cdfdee5d6dd6915383eb1)

Settings → Payments → Custom payment methods

![Custom payment methods list with the Create button highlighted](https://mintcdn.com/galepaymentsinc/fp_nidsvrE1p2bEe/images/stripe-cpm-4-create.png?fit=max\&auto=format\&n=fp_nidsvrE1p2bEe\&q=85\&s=19acaf8f24c2801325a58879acd0a7da)

Custom payment methods → Create

**2. Set the name and logo**

Gale is not in Stripe's catalogue of known methods, so choose **Provide a
custom name and icon**, then fill in:

| Field | Value |
| - | - |
| **Display name** | `Pay with FSA/HSA` |
| **Logo** | [Download the Gale logo](https://docs.withgale.com/images/gale-cpm-logo.png) |
| **Checkout Sessions endpoint URL** | Leave empty on this path. The [Checkout Session](https://docs.withgale.com/custom-payment-methods/elements-checkout-session.md) path is the one that sets it. |

Accept both sets of terms, then click **Create**.

![Create custom payment method panel with the custom name and icon link highlighted](https://mintcdn.com/galepaymentsinc/fp_nidsvrE1p2bEe/images/stripe-cpm-5-custom-name.png?fit=max\&auto=format\&n=fp_nidsvrE1p2bEe\&q=85\&s=7489e2b35c454d54a798f9654fd03d16)

Create custom payment method → Provide a custom name and icon

![Create custom payment method form showing display name, logo upload, Checkout Sessions endpoint URL and the terms checkbox](https://mintcdn.com/galepaymentsinc/fp_nidsvrE1p2bEe/images/stripe-cpm-6-details.png?fit=max\&auto=format\&n=fp_nidsvrE1p2bEe\&q=85\&s=493ef92cdd6438bb81aa504585c203f4)

Display name, logo, and the terms checkbox

**3. Copy the cpmt\_ id**

Stripe shows the **custom.type ID**, beginning `cpmt_`. Copy it. Your
frontend passes it to Stripe.js. Stripe displays it only once.

![Confirmation panel showing the custom.type ID to copy](https://mintcdn.com/galepaymentsinc/fp_nidsvrE1p2bEe/images/stripe-cpm-7-created.png?fit=max\&auto=format\&n=fp_nidsvrE1p2bEe\&q=85\&s=9c476fa71f1d02b4b5a78325be6110a8)

Custom payment method created — note the cpmt\_ id

## Step 2: Save the restricted key in Gale

Open **Integrations → Gale on Stripe** and paste the `rk_…` key. Gale uses it to write the payment record, and the page will not save unless Payment Records read and write are both granted. Paste the `cpmt_…` id from Step 1. Leave the signing secret blank. The [Checkout Session](https://docs.withgale.com/custom-payment-methods/elements-checkout-session.md) path is the one that uses that secret.

The walkthrough is on [Gale on Stripe](https://docs.withgale.com/custom-payment-methods/gale-on-stripe.md).

## Step 3: Add Gale to your checkout

Two changes in your frontend, and one new endpoint on your backend.

Your backend is what calls Gale, so it authenticates with your **API key**
(`glm_test_…` / `glm_live_…`). Generate one in the Gale Merchant Dashboard under
**Developer Tools → API Keys** — see [Getting your API
key](https://docs.withgale.com/developer-manual/authentication.md#getting-your-api-key). Keep it server-side;
it must never reach the browser.

Test and live are the key prefix. `glm_test_…` uses your Adyen test gateway. `glm_live_…` is a real charge. A `test_mode` field on the body is ignored.

**1. Frontend — register the CPM**

Stripe's deferred Elements setup — *Collect payment details before creating an
Intent*, [Collect payment details](https://docs.stripe.com/payments/accept-a-payment-deferred?platform=web\&type=payment#collect-payment-details) —
gains one option: [`customPaymentMethods`](https://docs.stripe.com/js/elements_object/create#stripe_elements-options-customPaymentMethods),
documented for the Payment Element
[here](https://docs.stripe.com/payments/payment-element/custom-payment-methods?payment-ui=elements).

```jsx Stripe's deferred Elements theme={null}
const options = {
  mode: 'payment',
  amount: 1099,
  currency: 'usd',
  appearance: {/*...*/},
};

<Elements stripe={stripePromise} options={options}>
  <CheckoutForm />
</Elements>
```

```jsx With Gale {5-8,12} theme={null}
const options = {
  mode: 'payment',
  amount: 1099,
  currency: 'usd',
  appearance: {/*...*/},
  customPaymentMethods: [{
    id: customPaymentMethodId,   // 'cpmt_...'
    options: { type: 'static', subtitle: 'Pay with your HSA/FSA card' },
  }],
};

<Elements stripe={stripePromise} options={options}>
  <CheckoutForm customMethodId={customPaymentMethodId} />
</Elements>
```

Custom payment methods render **last** in the Element by default. To put Gale
above the card options, pass `paymentMethodOrder: ['cpmt_...', 'card']` when
you create the Payment Element.

**2. Frontend — branch on submit**

Stripe's handler — *Submit the payment to Stripe* on the same
[deferred-Elements page](https://docs.stripe.com/payments/accept-a-payment-deferred?platform=web\&type=payment#submit-payment) —
validates, mints the PaymentIntent, then confirms with
[`stripe.confirmPayment`](https://docs.stripe.com/js/payment_intents/confirm_payment).
Gale adds a branch *before* the intent is created, and leaves the card path
untouched.

```jsx Stripe's deferred Elements theme={null}
const handleSubmit = async (event) => {
  event.preventDefault();
  if (!stripe) return;

  const { error: submitError } = await elements.submit();
  if (submitError) { handleError(submitError); return; }

  const res = await fetch('/create-intent', { method: 'POST' });
  const { client_secret: clientSecret } = await res.json();

  const { error } = await stripe.confirmPayment({
    elements,
    clientSecret,
    confirmParams: { return_url: 'https://example.com/order/123/complete' },
  });

  if (error) handleError(error);
};
```

```jsx With Gale {5,8-17} theme={null}
const handleSubmit = async (event) => {
  event.preventDefault();
  if (!stripe) return;

  const { error: submitError, selectedPaymentMethod } = await elements.submit();
  if (submitError) { handleError(submitError); return; }

  if (selectedPaymentMethod === customMethodId) {
    const { redirect_url } = await fetch('/create-gale-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ /* cart items, customer, ... */ }),
    }).then((r) => r.json());

    window.location.href = redirect_url;   // -> Gale hosted checkout
    return;                                // no PaymentIntent is created
  }

  const res = await fetch('/create-intent', { method: 'POST' });
  const { client_secret: clientSecret } = await res.json();

  const { error } = await stripe.confirmPayment({
    elements,
    clientSecret,
    confirmParams: { return_url: 'https://example.com/order/123/complete' },
  });

  if (error) handleError(error);
};
```

| Change | Why |
| - | - |
| Read `selectedPaymentMethod` from `elements.submit()` | Tells you which method the shopper picked. Stripe's version ignores it because there is only one path. |
| Branch when it equals your `cpmt_...` | Gale's leg is a redirect, not a Stripe confirmation. |
| `return` before `/create-intent` | No PaymentIntent should exist for a payment Gale is collecting — creating and confirming one would charge the shopper twice. |

**3. Backend — create the Gale checkout**

There is no separate create-cart call. This request creates the checkout and returns the hosted URL.

```js theme={null}
const cart = {
  reference_id: `order-${Date.now()}`,
  customer: { email: 'shopper@example.com', first_name: 'Sam', last_name: 'Shopper', phone: '+15555550123' },
  line_items: [
    { product_id: 'SKU-123', name: 'Blood Pressure Monitor', price: 2599, quantity: 1, currency: 'USD' },
  ],
  shipping_info: { address_line_1: '1 Test St', city: 'Austin', state: 'TX', postal_code: '73301', country: 'US' },
  metadata: {
    payment_source: 'stripe_elements_cpm',
    display_name: 'Pay with FSA/HSA',
  },
  success_url: 'https://your-site.com/success',   // HTTPS required
  failure_url: 'https://your-site.com/cancel',    // HTTPS required
};

const res = await fetch(`${GALE_API_URL}/api/v2/checkout`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${GALE_MERCHANT_API_KEY}`,   // glm_test_... / glm_live_... (SECRET, server-side only)
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify(cart),
});
const data = await res.json();
return { redirect_url: data.data.checkout_url };   // { data: { checkout_url, checkout_id } }
```

`checkout_id` looks like `cs_…`. That is Gale's checkout id, not a Stripe Checkout Session.

## Step 4: Set your Gale webhook URL

Gale sends the outcome to your application. In the Gale dashboard, open **Developer Tools → Webhooks**, add your endpoint, and subscribe to the events you fulfill on.

| Event | Fires when |
| - | - |
| `order.completed` | The payment is captured. Fulfill on this event, not on the browser redirect. |
| `order.failed` | The order is closed and will not complete |
| `payment.failed` | One attempt was declined. The shopper may retry the same checkout. |
| `refund.created` | You requested a refund |
| `refund.succeeded` | The refund was accepted |
| `refund.failed` | The refund failed |
| `dispute.created` / `dispute.closed` | A chargeback opened or resolved. Stripe has no `charge.*` event for this payment. |
| `checkout.expired` | The hosted page timed out unpaid |

Verify `X-Gale-Signature` on every delivery. Gale still sends `Authorization: Bearer` with the endpoint secret. That header will be removed, so use the signature as your check. See [Webhooks](https://docs.withgale.com/developer-manual/webhooks.md).

## Test vs live

Sandbox and live each need their own `cpmt_` id. The Gale key carries the environment: `glm_test_…` or `glm_live_…`.

## Scope of this page

This checkout charges once. Leave `mode` and `plan_id` off the request. For a subscription, use [Subscriptions](https://docs.withgale.com/subscriptions/overview). For a Stripe Checkout Session, use [Stripe Checkout](https://docs.withgale.com/custom-payment-methods/elements-checkout-session.md). If the subscription lives in your own Stripe Billing, use [Stripe Billing Subscriptions](https://docs.withgale.com/custom-payment-methods/stripe-billing.md).

## Charging the same card again

To charge the customer again later from your own system, for a renewal or a refill, add `"setup_future_usage": "off_session"` to the checkout you create in Step 3. The shopper agrees on Gale's page, Gale returns a payment method id, and you charge it by API. Send the same `payment_source` in `metadata` on each of those charges and Gale writes them to your Stripe account too. See [Saved Payment Methods](https://docs.withgale.com/integration/saved-payment-methods.md).

## Refunds

Refund the Gale order with [`POST /api/v2/refunds`](https://docs.withgale.com/api-reference/endpoint/create-refund). `order_id` is required. `amount` is optional and is in cents.

Gale then sends `refund.created`, `refund.succeeded`, or `refund.failed`. Stripe does not send `charge.refunded` for this payment. Do not refund it from the Stripe Dashboard.

## Failure modes

* **Wrong or missing API key, or a key from the other environment** → `401`/`422` from `/api/v2/checkout`.
* **Line item missing `product_id`** → `422`. It's required on every line item. A `product_id` that Gale doesn't recognise as HSA/FSA-eligible is accepted — that item just charges on the non-eligible leg.
* **`success_url` / `failure_url` not HTTPS** → checkout creation is rejected. Use a tunnel or a local certificate in development.
* **Payments are not configured for this environment** → `CHECKOUT_001`. No checkout is created.

## Test your integration

**1. Use test credentials**

Use your `glm_test_…` Gale key and your test-mode `cpmt_…` id. Environment
is encoded in the key prefix — nothing else about the request changes.

**2. Confirm the option renders**

Load your checkout: **Pay with FSA/HSA** appears after your native methods,
with the name and logo you gave the type in Stripe.

If it does not, the problem is Stripe-side — wrong mode, wrong account, or a
`cpmt_` id that does not belong to the publishable key mounting the Element.
Also check that Adaptive Pricing is not active. No Gale setting changes this.

**3. Confirm the branch skips stripe.confirmPayment**

Select Gale and submit. Your handler should branch before Stripe's confirm
call, so no PaymentIntent is created or confirmed — if one appears in your
Stripe logs, the branch is not firing.

**4. Create a checkout**

Your backend endpoint calls `POST /api/v2/checkout` and answers
`{ data: { checkout_url, checkout_id } }`, and the shopper lands on the
Gale-hosted card page.

**5. Run a mixed cart**

Include an item whose `product_id` Gale does not recognise as eligible. It
should still charge, on the non-eligible leg, rather than being rejected.

**6. Confirm the outcome server-side**

Verify `order.completed` on your own backend before fulfilling. The order
appears in your Gale dashboard under **Orders** with the `reference_id` you
sent. Gale reports a payment record to Stripe with the restricted key from
Step 2. There is no Stripe Charge.

**7. Test cancel and decline**

Abandon the Gale page and confirm the shopper returns to your `failure_url`
and can pay another way, with the order left unfulfilled.

> [!WARNING]
>
> **There is no PaymentIntent and no Charge for a Gale payment.** So a
> `payment_intent.succeeded` or `charge.succeeded` handler will never fire for
> one. Fulfill from `order.completed`. The browser redirect is not something
> your server can verify.

## Go-live checklist

* Live Gale key (`glm_live_…`) on the live code path, and the live `cpmt_` id in the Element
* Restricted key saved on Gale on Stripe, with Payment Records read and write
* Endpoint URL on the custom payment method left empty
* `product_id` present on every line item
* `success_url` and `failure_url` are HTTPS
* `order.completed` verified before fulfilment, with `X-Gale-Signature` checked
* One real live transaction in the Gale dashboard, and the payment record in Stripe
* Refunds go through `POST /api/v2/refunds`, not the Stripe Dashboard

## Related resources

> [card] [Create Checkout](https://docs.withgale.com/api-reference/endpoint/create-checkout-v2.md)
>
> Every field on the cart, its rule, and the response shape.

> [card] [Webhooks](https://docs.withgale.com/developer-manual/webhooks.md)
>
> The signature header, the event list, and retries.

> [card] [Create a refund](https://docs.withgale.com/api-reference/endpoint/create-refund.md)
>
> `POST /api/v2/refunds`.

> [card] [Gale on Stripe](https://docs.withgale.com/custom-payment-methods/gale-on-stripe.md)
>
> The restricted key Gale uses to report the payment back.

> [card] [Subscriptions](https://docs.withgale.com/subscriptions/overview)
>
> Billing, the event for each cycle, and refunds.

> [card] [Stripe Checkout](https://docs.withgale.com/custom-payment-methods/elements-checkout-session.md)
>
> Stripe calls Gale at the custom payment method endpoint URL.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.