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 withpayment_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.
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: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 keepsnumberMinWidth — 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) andhttps://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.
frame-src:https://js.verygoodvault.com, for the secure frames.connect-src: the collection host the session names incollect.host(read it from the session your page opens; the vault’s own hostname whencollect.hostisnull), through which the confirm is submitted, andhttps://vgs-collect-keeper.apps.verygood.systems.
Where it runs
A session is bound to the workspace and environment of the key that created it, so apk_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.