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

# Overview

> Wallet buttons on your page or on the hosted checkout. The customer confirms on their device, the wallet hands Payra a token, and the payment reads back as a card payment.

Apple Pay and Google Pay let a customer pay with a card kept on their device. Instead of typing a number, they
confirm with Face ID, Touch ID, a fingerprint or the passcode. The wallet hands over a token encrypted for Payra's
processor, so your page never sees card data and Payra never reads the token. The result is an ordinary card
[payment](https://docs.payra.com/payments.md) with the same declines, 3-D Secure, refunds and webhooks. Only `card.wallet` tells it apart.

Payra enables wallets per workspace. They work through [Payra Elements](https://docs.payra.com/elements.md) on your own page and on the
[hosted checkout](https://docs.payra.com/hosted-checkout.md). The first part of this page applies to both wallets. [Google Pay](https://docs.payra.com/google-pay.md)
and [Apple Pay](https://docs.payra.com/apple-pay.md) cover what is specific to each.

## Before you start

* **Keep a card form on the page.** Elements draws a wallet's button only where the device can pay with it. A
  device that cannot pay with one wallet may pay with the other, so offer both and show what comes back
  `available`, as the hosted checkout does.
* **A wallet already holds the customer's saved cards.** Apple Pay and Google Pay keep them on the customer's
  device and account, so a returning customer pays by tapping the button again. A wallet card pays once: the
  charge refuses `save_payment_method`, and `POST /payment-methods` refuses `usage: "saved"`. To save a card,
  collect it with a card session.
* **The amount is fixed when the session is created.** The sheet (the wallet's own payment screen) shows it, the
  customer confirms it, and the charge must be for exactly that amount and currency.

| | Elements, on your page | Hosted checkout |
| - | - | - |
| Google Pay | Yes, on a [registered domain](https://docs.payra.com/google-pay.md#register-your-domains) | Yes |
| Apple Pay | Yes, on a [registered domain](https://docs.payra.com/apple-pay.md#register-your-domains) | Yes |
| Currencies | `USD`, `CAD` | `USD`, `CAD` |
| Networks | Visa, Mastercard, American Express, Discover | Same |

## Accept a wallet payment

**1. Create a session for each wallet**

One session collects one wallet and carries the amount its sheet shows, in the smallest unit of `currency`
(`USD` when omitted, or `CAD`). To show both buttons, create two sessions.

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/payment-method-sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: 4b7c1d2e-8f3a-4c5b-9d6e-0a1b2c3d4e5f" \
  -H "Content-Type: application/json" \
  -d '{ "payment_method_types": ["google_pay"], "amount": 12550, "currency": "USD" }'
```

```json Response (abbreviated) theme={null}
{
  "object": "payment_method_session",
  "id": "pms_test_7Hs2kQ9mL1pXv4cR8tWzAbCd",
  "client_secret": "pms_test_7Hs2kQ9mL1pXv4cR8tWzAbCd_secret_...",
  "status": "requires_payment_method",
  "payment_method_types": ["google_pay"],
  "amount": 12550,
  "currency": "USD",
  "card": null,
  "expires_at": "2026-10-29T18:30:00.000Z"
}
```

`apple_pay` works the same way. Three things are refused before any page opens: a session without `amount`
(`400 parameter_invalid` on `amount`), a currency other than `USD` or `CAD` (`400 parameter_invalid` on
`currency`), and a wallet your workspace does not offer (`400 payment_provider_not_configured` on
`payment_method_types`). Hand the page each session's `client_secret` and nothing more.

**2. Mount the buttons**

Each wallet gets its own `elements()` call, with its own session.

```html theme={null}
<div id="apple-pay-element" hidden></div>
<div id="google-pay-element" hidden></div>
<div id="card-element"></div>
<p id="hint"></p>
<script src="https://api-dashboard.payra.com/v1/payra.js"></script>
<script>
  const payra = Payra('pk_test_...');
  const hint = document.querySelector('#hint');

  function mountWallet(type, clientSecret, slot) {
    const element = payra.elements({ clientSecret }).create(type, { buttonType: 'pay' });
    element.on('ready', ({ available }) => {
      document.querySelector(slot).hidden = !available;   // nothing drawn when the device cannot pay
    });
    element.on('confirmed', async ({ paymentMethodSession }) => {
      hint.textContent = '';
      const payment = await chargeOnYourServer(paymentMethodSession.id); // POST /payments, same amount
      const { redirected, error } = payra.handleNextAction(payment);     // 3-D Secure, if the bank asks
      if (redirected || error) return;
      if (payment.status !== 'failed') return showOutcome(payment.status);
      hint.textContent = payment.failure.message;
      element.destroy();                                                 // a session pays once, so swap it
      mountWallet(type, await newSessionOnYourServer(type), slot);       // POST /payment-method-sessions
    });
    element.on('error', ({ error }) => {
      hint.textContent = error.message;
    });
    element.mount(slot);
  }

  mountWallet('apple_pay', 'pms_test_..._secret_...', '#apple-pay-element');
  mountWallet('google_pay', 'pms_test_..._secret_...', '#google-pay-element');
</script>
```

There is no form to submit. The customer confirms in the sheet, which shows the amount and your business name.
Elements then confirms the session with the wallet's token and fires `confirmed`, the moment to charge. After a
decline the page swaps the spent session for a new one, since a session pays once.

**3. Charge on your server**

Send the session as `payment_method_session`, with its amount.

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: 9d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_method_session": "pms_test_7Hs2kQ9mL1pXv4cR8tWzAbCd",
    "amount": 12550,
    "currency": "USD",
    "reference": "1042",
    "return_url": "https://shop.example/orders/1042/paid"
  }'
```

```json Response (abbreviated) theme={null}
{
  "object": "payment",
  "id": "pay_test_7Hs2kQ9mL1pXv4cR8tWzAbCd",
  "status": "succeeded",
  "amount": 12550,
  "currency": "USD",
  "payment_method_type": "card",
  "card": { "brand": "visa", "last4": "1111", "exp_month": null, "exp_year": null, "wallet": "google_pay" }
}
```

The payment answers `succeeded`, a decline, or `requires_action`, as a typed card does. An amount or currency
other than the session's is `400 parameter_invalid` on `amount`, and nothing reaches the processor.

A session has two 30-minute windows. The customer has 30 minutes from its creation to pay in the sheet, and
you have 30 minutes from `confirmed` to charge it. A later charge answers `payment_method_session_inactive`. A
session pays once, approved or declined, and a second charge answers `payment_method_session_used`.

`requires_action` means the card's bank wants to verify the customer. Send `return_url` on every wallet charge:
on a workspace that offers wallets it always takes effect, and without it the customer cannot be sent to the
bank. `handleNextAction` follows the payment's `redirect`, and the customer comes back to your `return_url`.
The rest of [3-D Secure](https://docs.payra.com/payments.md#3-d-secure) applies.

## The button

Each wallet draws its own button with Google's and Apple's own libraries, so it always matches their brand rules.

```js theme={null}
elements.create('apple_pay', {
  buttonType: 'pay',          // 'plain' (logo only, the default) | 'buy' | 'pay' | 'checkout' | 'book' | 'donate' | 'order' | 'subscribe'
  buttonStyle: 'black',       // 'black' (default) | 'white' | 'white-outline'
  theme: { fieldHeight: '48px', borderRadius: '8px' },
});
```

| Option | What it does |
| - | - |
| `buttonType` | The word beside the logo. Each wallet renders its own version of the same list. |
| `buttonStyle` | The outlined style is Apple's. Google draws it as its white button. |
| `theme` | The card element's `theme`. The button is as tall as `fieldHeight` (`44px` by default) and as round as `borderRadius` (`8px`). The other keys do not apply. |

The button fills the width of the container you mount into, so size the container. For anything the theme does
not cover, the container has the classes `PayraElement` and `PayraElement--wallet`, and the button has
`PayraElement-wallet-button` plus `PayraElement-wallet-button--google_pay` or `--apple_pay`. Place the buttons
where the customer expects an express option, above or beside the card fields, and follow
[Apple's](https://developer.apple.com/design/human-interface-guidelines/apple-pay) and
[Google's](https://developers.google.com/pay/api/web/guides/brand-guidelines) guidelines for the space around
them.

## Events and errors

| Event | Payload | When |
| - | - | - |
| `ready` | `{ available }` | The session was read and the device asked. With `true` the button is on the page. With `false` nothing was drawn, so offer another way to pay. |
| `click` | | The customer tapped the button and the sheet is opening. |
| `confirmed` | `{ paymentMethodSession }` | The wallet authorized the payment, Payra holds its token and the sheet closed. Charge it from your server. |
| `cancel` | | The customer closed the sheet without paying. Nothing was charged, and the button still works. |
| `error` | `{ error }` | The sheet could not finish (codes below). Google keeps its sheet open, so `confirmed` or `cancel` can still follow. Apple closes its sheet (see [Apple Pay errors](https://docs.payra.com/apple-pay.md#apple-pay-errors)). |
| `loaderror` | `{ error }` | The session could not be opened (wrong key or client secret, or a session that expired, was canceled or already succeeded), or it is not a session for this wallet. |

`payra.confirmPaymentMethodSession(elements)` is not needed with a wallet. On a wallet element it submits nothing,
and answers the session `confirmed` carried, or `wallet_not_authorized` before the customer pays.

| `type` | `code` | When |
| - | - | - |
| `api_error` | `wallet_failed` | The wallet could not complete the sheet. The message carries the wallet's own reason. |
| `api_error` | `network_error` | Payra could not be reached. Worth one retry. |
| `invalid_request_error` | `session_spent` | The session was confirmed already, for example when the customer tapped again after paying. Create a new session. |
| `invalid_request_error` | `payment_method_session_inactive` | The session stopped collecting while the page was open: it expired (30 minutes after it was created), was canceled or already succeeded. Create a new session. |
| `invalid_request_error` | `wallet_token_invalid` | The token did not have the shape the wallet documents. Not expected on an unmodified page, since Elements sends it as the wallet handed it. |
| `invalid_request_error` | `session_not_wallet`, `session_amount_missing` | On `loaderror`. The session does not collect this wallet, or carries no amount. |
| `api_error` | `wallet_config_missing`, `elements_load_failed` | On `loaderror`. Your workspace stopped offering the wallet after the session was created, or the button failed to load. Offer another way to pay. |
| `invalid_request_error` | `wallet_not_authorized`, `element_not_mounted` | From `confirmPaymentMethodSession`. The customer has not paid yet, or the button is not on the page. |

Apple Pay adds [three codes of its own](https://docs.payra.com/apple-pay.md#apple-pay-errors). Errors from the charge are under
[Payments](https://docs.payra.com/payments.md#errors).

## What you read back

The payment, the payment method it made and every `payment.*` [webhook](https://docs.payra.com/webhooks.md) carry the card as the wallet
disclosed it:

```json theme={null}
"card": { "brand": "visa", "last4": "1111", "exp_month": null, "exp_year": null, "wallet": "apple_pay" }
```

`exp_month` and `exp_year` are always `null` on a wallet card. `brand` and `last4` are `null` only when the wallet
did not disclose them. `billing_details.name` is `null`, since a wallet does not hand over the name. Read `wallet` as
an open string, since the list may grow. Refund a wallet payment as [any card payment](https://docs.payra.com/refunds.md).

## On the hosted checkout

When your workspace offers wallets and a [checkout session](https://docs.payra.com/hosted-checkout.md) offers `card`, the page shows the
wallet buttons above the payment form, Apple Pay first, and only those the customer's device can pay with. There
is nothing to send. `payment_method_types` never lists wallets, and a checkout without `card` shows none.

The customer taps, confirms on the device, and the page charges and finishes as with a card. The customer lands
on `success_url` with `?checkout_session=cs_…`, the session's `payment` carries `card.wallet`, and
`checkout_session.completed` and `payment.succeeded` are sent. A refused wallet payment shows its reason under
the buttons, and the customer can tap again or pay with the form.

## Testing

In sandbox the sheets are real and no money moves. [Test Google Pay](https://docs.payra.com/google-pay.md#test-google-pay) and
[Test Apple Pay](https://docs.payra.com/apple-pay.md#test-apple-pay) say how to pay in each wallet's test environment. Check the payment for
`card.wallet`, and that `payment.succeeded` reached you. A wallet never asks for a name, so the name-on-card
refusals on [Testing](https://docs.payra.com/testing.md) do not apply. Try your decline path with a typed card.

## Content Security Policy

If your page sets one, add `https://pay.google.com` (Google Pay) and `https://applepay.cdn-apple.com` (Apple Pay)
to `script-src`, on top of what [Elements](https://docs.payra.com/elements.md#content-security-policy) needs. Then try one payment with
each wallet under your policy. A blocked host shows in the browser console as a Content Security Policy
violation.


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