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 backrequires_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 withreturn_url): the page leaves at once for Payra’s verification page or the bank’s, and resolves{ redirected: true }; the customer lands on yourreturn_urlafterwards.handleNextActionreturns 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, sinceredirectedanderrorare also on the promise. A verification on your page needs theawait.three_ds_fingerprintandthree_ds_challenge(a charge withoutreturn_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(withmessage, the payment’sfailure.message, which you can show) orprocessing. The customer never leaves your page. Closing the modal fails the payment withauthentication_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 }.
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 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.
Canadian bank accounts (EFT)
On a session created withpayment_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.
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: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 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 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) andhttps://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; yourframe-srcapplies 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.
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: 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.