Skip to main content
Payra Elements is the browser library of the Payra API. It mounts the card fields (number, expiration date, security code) or the US bank account fields (routing and account number, with the ACH authorization the account holder accepts) inside your page, each number in its own secure frame, and hands what it collected to a payment method session. Your page never sees card or bank data and never holds a secret key; the only key it carries is the publishable one.
1

Load the script

The library is served by the API itself, so it knows where the API is from where it was loaded. Nothing to configure.
2

Mount the card element

Create a session on your server first (it returns the client_secret), then:
change fires as the customer types: complete says the three fields are valid, error names the first field with a problem once the customer has left it, brand comes from the card number.
3

Confirm

On submit, hand Elements the name on the card (and the billing postal code, if you collect it):
Elements sends the card fields through the secure route, which replaces the number and the security code with tokens before they reach Payra, then closes the session. Send paymentMethodSession.id to your server: it creates the payment method from it with the secret key, and charges that. confirmPaymentMethodSession never rejects: a programming mistake (secret key in the page, malformed client secret, missing mount target) throws from Payra(), elements() or mount() instead.

Bank accounts

On a session created with payment_method_types: ["us_bank_account"], mount the bank account element instead. It draws the routing number and account number fields under their labels, the account type (checking / savings) and the account holder (individual / company) as radios, preselected on checking and individual, and the ACH authorization the customer accepts: the sentence Payra worded for your business (and, on a one-time session, its amount) beside an unticked checkbox, with the full text behind a “View full ACH Authorization” link that opens it in a dialog. The wording comes from the session; the page cannot change it.
The confirm sends the version of the text the customer saw with the tokenized numbers, and Payra stamps the moment and the address. While the box is unticked, Elements refuses the confirm with ach_authorization_required before anything is sent. placeholders (routing, account) and labels (routing, account, accountType, accountHolderType, viewAuthorization) reword the parts that are yours; the choices themselves (Checking, Savings, Individual, Company) and the dialog’s title and Close button are fixed. style and theme apply as they do to the card, and focusColor also colors the radios and the checkbox. The element lays itself out (the two number fields sit side by side when there is room), so layout does not apply. A card session given 'us_bank_account' arrives as loaderror with session_not_bank.

Styling

The text inside the fields, the boxes around them and how those boxes sit are all yours:
Elements loads no fonts into the secure fields, so a fontFamily in style only applies when the customer’s device has that font; a web font your page loads does not reach them. theme sets CSS variables on the container you mount into. To set them from your stylesheet instead, target the container with a selector stronger than one class (for example #card-element { --payra-focus-color: … }), since Elements declares every default on the container itself: The container gets the class PayraElement (plus PayraElement--bank for the bank account element); each field sits in a PayraElement-field that carries PayraElement--focus, PayraElement--invalid, PayraElement--complete and PayraElement--empty as the customer types (the state names Stripe.js uses, with the PayraElement prefix), for anything the variables do not cover. The bank element’s labels are PayraElement-label; each radio choice and the checkbox sit in a <label> with their text, PayraElement-radio and PayraElement-consent; the link is PayraElement-terms, and the full text opens in a PayraElement-dialog. The focus glow derives from focusColor, and a field with an error gets an errorColor border; the field boxes’ transitions turn off under prefers-reduced-motion.

How the fields fit

The card number never gives way to the other two. It keeps numberMinWidth — by default enough for a 16-digit number, its brand icon and the padding at the default font — and the row wraps instead of clipping it: the number takes a row of its own and the expiration date and security code drop below it together, in a PayraElement-group. With the defaults that happens under about 448px of container (450px if your page keeps the default box-sizing: content-box): the number’s 240px, the gap, and the 112px + 80px + gap the other two need side by side. Raise numberMinWidth if you set a wider font or take the 19-digit ranges; lower it if your font is narrower and you would rather keep one row. layout: 'stacked' skips the question and gives every field its own row, whatever the width.

Events and errors

Errors from confirmPaymentMethodSession use the API’s error shape: what the API answered, or one of these that Elements raises itself. A programming mistake throws an error named PayraSetupError (with the same error shape inside) from Payra(), elements(), create() or mount() instead: invalid_publishable_key, secret_key_in_browser, api_base_url_missing (the script tag did not tell Elements where the API is; pass the API’s origin, without /v1, as Payra('pk_…', { apiBaseUrl })), invalid_client_secret, unknown_element, mount_target_missing. A session the browser cannot open (wrong key or client secret, or a session that expired, was canceled or already succeeded), a session opened with the wrong element (session_not_bank for a card session on the bank account element, session_not_card for the reverse) and secure frames that fail to load arrive as the loaderror event (collect_missing, elements_load_failed, network_error when Payra could not be reached, or the API’s own code).

Content Security Policy

If your page sets one, allow:
  • script-src: the API host (payra.js) and https://js.verygoodvault.com, from which Elements loads the secure-field SDK.
  • connect-src: the API host, which Elements calls to read the session.
  • style-src 'unsafe-inline': Elements injects one <style data-payra-elements> for the field boxes, without a nonce.
The secure-field SDK also reaches these hosts, as seen in a browser during a checkout:
  • frame-src: https://js.verygoodvault.com, for the secure frames.
  • connect-src: the collection host the session names in collect.host (read it from the session your page opens; the vault’s own hostname when collect.host is null), through which the confirm is submitted, and https://vgs-collect-keeper.apps.verygood.systems.
Try one checkout under your policy before going live: a blocked host shows up in the browser console as a Content Security Policy violation.

Where it runs

A session is bound to the workspace and environment of the key that created it, so a pk_test_ page opens sandbox sessions and a pk_live_ page opens live ones. A sandbox workspace runs on a test processor, and the cards it approves, the ways to make it decline and the bank accounts it accepts are that processor’s, so ask Payra for the set that applies to your workspace; there is no Payra-wide test card. A forced decline reads exactly like a real one (402 card_declined with a decline_code), so it is the way to try your failure path. A sandbox bank debit settles on the sandbox processor’s own schedule rather than at once: leave the payment processing and let the payment.succeeded webhook tell you when it settled.