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

# Build a checkout with Payframes

> Collect card details in secure fields and process donations through your own backend.

Payframes lets you own the donation form while card details are collected inside
secure embedded fields. Your page receives tokenized card values, and your server
uses a secret key to process the donation. Tokenization alone never charges a card.

Before starting, [enable access](/external/onboarding/wegive-pay/setup-requirements),
register your website origin, and [install the SDK](/external/onboarding/wegive-pay/sdk-installation).
Use matching test publishable and secret keys from your organization.

## How the flow works

1. Your backend creates a donation attempt and records the donor-authorized
   amount, currency, and attribution.
2. Your browser mounts Payframes with the publishable key and collects donor and
   billing details outside the secure card fields.
3. Payframes tokenizes the card. Your page sends the returned values and fraud
   session to your backend over HTTPS.
4. Your backend validates the attempt, stores its exact processing request, and
   calls the WeGive API with its secret key and stable idempotency key.
5. Your application records the returned transaction ID and state, then shows
   success, pending, or an appropriate failure/reconciliation state.

There is no browser-side SDK charge method. Never call the secret-key processing
API directly from client JavaScript.

## Mount the form

This form assumes your server renders a unique attempt ID and CSRF token into the
placeholders. Adapt authentication and CSRF protection to your framework. The
amount comes from your backend's attempt record, not an untrusted hidden input.

```html theme={null}
<form id="donation-form" data-attempt-id="SERVER_CREATED_ATTEMPT_ID">
  <input type="hidden" name="csrf" value="YOUR_FRAMEWORK_CSRF_TOKEN" />
  <label>Email <input name="email" type="email" required /></label>
  <label>First name <input name="firstName" autocomplete="given-name" required /></label>
  <label>Last name <input name="lastName" autocomplete="family-name" required /></label>
  <label>Cardholder name <input name="cardholder" autocomplete="cc-name" required /></label>
  <label>Postal code <input name="postal" autocomplete="postal-code" required /></label>
  <p id="number-label">Card number</p>
  <div id="card-number" class="payframe" role="group" aria-labelledby="number-label"></div>
  <p id="expiry-label">Expiration</p>
  <div id="card-expiry" class="payframe" role="group" aria-labelledby="expiry-label"></div>
  <p id="cvv-label">Security code</p>
  <div id="card-cvv" class="payframe" role="group" aria-labelledby="cvv-label"></div>
  <button type="submit" disabled>Donate</button>
  <p id="payment-status" role="status" aria-live="polite"></p>
</form>
<style>
  .payframe { height: 44px; border: 1px solid #737373; border-radius: 6px; }
</style>
```

After loading the SDK, run this browser code. `/donation-attempts/submit` is **your
application's route**, not a WeGive endpoint. The response shape used here is
also an example contract for your own backend.

```js theme={null}
const pay = WeGive.Pay.init({ publishableKey: 'pk_test_REPLACE_WITH_YOUR_KEY' });
const form = document.querySelector('#donation-form');
const submit = form.querySelector('button[type="submit"]');
const message = document.querySelector('#payment-status');
let fields;
let busy = false;
let sentToBackend = false;

async function initializeFields() {
  try {
    fields = await pay.payframes.mount({
      cardNumber: '#card-number',
      cardExpiry: '#card-expiry',
      cardCvv: '#card-cvv',
      checkoutAttemptId: form.dataset.attemptId,
      style: { 'font-size': '16px', color: '#17251e' },
    });
    await fields.ready();
    submit.disabled = false;
  } catch {
    message.textContent = 'Payment fields could not load. Please contact us if this continues.';
  }
}

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  if (busy || sentToBackend || !fields) return;
  if (!fields.isValid()) {
    message.textContent = 'Please complete all card fields.';
    return;
  }
  busy = true;
  submit.disabled = true;
  try {
    const tokenized = await fields.tokenize();
    // Obtain fresh context for this first submission, even if the form sat open.
    const session = await pay.createFraudSession({ checkoutAttemptId: form.dataset.attemptId });
    const data = new FormData(form);
    const body = JSON.stringify({
      attemptId: form.dataset.attemptId,
      tokenized: { ...tokenized, fraudSessionId: session.id },
      donor: {
        email: data.get('email'),
        firstName: data.get('firstName'),
        lastName: data.get('lastName'),
      },
      cardholder: { name: data.get('cardholder'), zip: data.get('postal') },
      utm: pay.getAttribution(),
    });

    // After this point, a lost response does not mean that payment failed.
    sentToBackend = true;
    const response = await fetch('/donation-attempts/submit', {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': String(data.get('csrf')),
      },
      body,
    });
    if (!response.ok) throw new Error('Backend response requires review');
    const result = await response.json();
    if (result.outcome === 'success') {
      message.textContent = `Thank you! Reference: ${result.transactionId}.`;
    } else if (result.outcome === 'pending') {
      message.textContent = 'Your payment is awaiting confirmation.';
    } else {
      message.textContent = 'Please check this donation attempt before submitting another payment.';
    }
  } catch {
    message.textContent = sentToBackend
      ? 'We could not confirm the result. Please check this attempt before paying again.'
      : 'Payment details could not be submitted. Please try again or contact us.';
  } finally {
    busy = false;
    submit.disabled = sentToBackend;
  }
});

void initializeFields();
```

Once submitted, keep the form from creating a second attempt while the first
outcome is unknown. Your backend should offer a way to retrieve **its saved
attempt state** after a reload. Only offer a fresh attempt after confirming that
it will not duplicate the donor's payment. Remove your listeners and call
`fields.destroy()` when leaving the form; see
[asynchronous cleanup](/external/onboarding/wegive-pay/versions-and-lifecycle#cleanup-in-a-single-page-application).

## Implement your backend route

Your route should:

1. Authenticate or otherwise validate the donor's authorized checkout session,
   apply CSRF protection where applicable, and load its server-side attempt.
2. Verify that the attempt belongs to this checkout and that its donation choices
   are allowed. Do not trust client-supplied amount, customer IDs, or attribution IDs.
3. Atomically claim an unsubmitted attempt so two requests cannot both start it.
   For an existing attempt, return its saved state or reconcile its existing request.
4. Build an inline charge or a by-reference charge as described in the
   [API reference](/external/onboarding/wegive-pay/processing-api).
5. Save the exact serialized request, endpoint, original browser `User-Agent`,
   secret-key identifier, and idempotency key before submitting it. Keep sensitive
   request material in protected server-side storage, out of ordinary logs.
6. Send the request, record the response, and return a minimal result to the page.
   A timeout or 5xx response must leave the attempt unresolved until reconciled.

Forward the initiating browser's `User-Agent` on processing requests and preserve
it for retries so the request retains its checkout context. Do not replace it with
your HTTP client's default agent. Do not accept arbitrary client-supplied forwarding
headers as trusted IP identity.

For the inline path, map `tokenized.cardNumber`, `cardExpiration`, and `cardCvv`
into the API's `tokens` object, and place `tokenized.fraudSessionId` at the top
level. Donor details go into `donor`; billing name and postal code go into
`cardholder`. The backend supplies `amount`, `currency`, `frequency`, and approved
attribution. Payframes does not add attribution to your server request
automatically: validate and forward the values returned by `getAttribution()`.

Do not log credentials, raw card data, tokenized payment values, fraud-session
handles, or full request bodies. Retain the transaction ID, attempt ID, timestamp,
and sanitized error information for support and reconciliation.

## Field styling and validation

Host-page CSS controls the containers; it cannot style inside the secure frames.
Use the mount call's `style` object for field CSS. The SDK also reads these CSS
variables from your page at mount time:

```css theme={null}
:root {
  --wegive-pay-color: #17251e;
  --wegive-pay-placeholder: #58675f;
  --wegive-pay-bg: #ffffff;
}
```

Styles are resolved when fields mount. For a changed theme, destroy and remount
the fields, allowing the donor to re-enter their details. `ready()` means the
fields mounted; use `isValid()` before tokenizing and still handle tokenization
or processing failures. Test keyboard navigation, labels, zoom, and mobile layouts.

## Recurring donations and bank methods

For recurring donations, create a customer, profile, and payment method, then
call `POST /api/pay/recurring-plans`. This can take the first payment immediately.
Do not separately create a one-time transaction for the same initial payment.
See [recurring plans](/external/onboarding/wegive-pay/processing-api#recurring-plans).

Payframes exposes card fields. The processing API accepts bank methods collected
through an approved bank-tokenization integration; it does not provide a bank
field mount method in this SDK. Use the hosted pop-up for its enabled bank flow
or contact WeGive to configure your custom bank integration. Never substitute
ordinary browser inputs that send raw bank details through your backend.


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