Skip to main content
Payra Elements is the browser library of the Payra API. It mounts the card fields (number, expiration date, security code), the US bank account fields (routing and account number, with the ACH authorization the account holder accepts) or the Canadian ones (institution, transit and account number, with the pre-authorized debit agreement) 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 charges it in one call with the secret key, sending it as payment_method_session. confirmPaymentMethodSession never rejects: a programming mistake (secret key in the page, malformed client secret, missing mount target) throws from Payra(), elements() or mount() instead.

3-D Secure

The payment your server charged can come back requires_action: the card’s bank wants to verify the customer (see Payments). Pass it to handleNextAction, which does the step and nothing for any other payment:
  • next_action.type: "redirect" (a charge you sent with return_url): the page leaves at once for Payra’s verification page or the bank’s, and resolves { redirected: true }; the customer lands on your return_url afterwards.
  • handleNextAction returns a promise. A page written when it was synchronous (const { redirected, error } = payra.handleNextAction(payment)) keeps working for a redirect, a payment with nothing to do and a step it cannot take, since redirected and error are also on the promise. A verification on your page needs the await.
  • three_ds_fingerprint and three_ds_challenge (a charge without return_url): Elements loads the bank’s check in a hidden frame, then, when the bank asks, its challenge in a modal with a close button, and resolves once the bank answered with { completed: true, status }: succeeded, failed (with message, the payment’s failure.message, which you can show) or processing. The customer never leaves your page. Closing the modal fails the payment with authentication_failed; the card is spent, so collect it again with a new session. The same payment passed again waits on the verification already running.
  • Any other status resolves { redirected: false }.
The same call runs the bank’s step for a card you save: pass it the payment method your server created with use_payra_js: true. The modal reads “Confirm your card”, and status is succeeded once the card is saved or failed when it was not; read the payment method on your server before you charge it. A method that came back active with a next_action (a card the customer already has, collected again with new details) runs the same way: succeeded means it took the new details, failed that it keeps the ones it has. It reads only status and next_action, follows https redirects only and frames only Payra’s own pages, and never rejects: an action it cannot take resolves { redirected: false, error } with next_action_unsupported; a verification whose outcome could not be read, next_action_unconfirmed (read the payment on your server before charging again: it may have gone through). Elements sends the customer’s browser with every card confirm, which is what lets the charge be verified on the page.

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.

Canadian bank accounts (EFT)

On a session created with payment_method_types: ["acss_debit"] (in CAD; see Payment method sessions), mount elements.create('acss_debit'). It is the bank account element for a Canadian account: the institution number (3 digits), the transit number (5 digits) and the account number (7 to 12 digits), each in its own secure frame under its label, the same two choices, and the pre-authorized debit (PAD) agreement the customer accepts: the sentence Payra worded for your business beside an unticked checkbox, with the full agreement behind a “View full Pre-Authorized Debit Agreement” link that opens it in a dialog. The wording comes from the session’s pad_agreement; the page cannot change it.
Everything else works as on the US element. No billing postal code is sent, since a Canadian debit takes none, so billingDetails.postalCode is ignored. While the box is unticked, Elements refuses the confirm with pad_agreement_required before anything is sent. placeholders take institution, transit and account; labels take the same three plus accountType, accountHolderType and viewAuthorization. The three number fields sit side by side when there is room and wrap when there is not. A US bank account session given 'acss_debit' arrives as loaderror with session_not_acss, and a Canadian session given 'us_bank_account' with session_not_bank. The debit is processing until the processor settles it, then succeeded, as a US one is: see Bank debits. In sandbox a Canadian debit can stay processing, so test the rest of your flow without waiting for payment.succeeded. The agreement covers this one debit “for the amount shown” and the element does not show the amount, so keep the amount on your page next to the fields, as any checkout does.

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 either 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 session that is not a US bank account one on the US bank account element, session_not_acss for one that is not Canadian on the Canadian element, session_not_card for a bank session on the card element) 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 and to run 3-D Secure.
  • frame-src: https:. The 3-D Secure frames are the API’s, and a challenge then loads the card’s bank’s own page inside that frame, at an address that differs from bank to bank; your frame-src applies to it too.
  • style-src 'unsafe-inline': Elements injects one <style data-payra-elements> for the field boxes and the 3-D Secure modal, 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: see Testing for the set, including the name-on-card values that force each decline and the 3-D Secure cards. 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 (a Canadian one can stay processing): leave the payment processing and let the payment.succeeded webhook tell you when it settled.