[woocommerce_checkout] shortcode and the Checkout block: card, US bank account (ACH) and
Canadian bank account (EFT) fields inside your checkout, and Pay with Payra, which sends the
customer to a hosted checkout page and back. Card and bank details are entered in
secure fields rendered by Payra Elements and tokenized before they reach Payra; your store
never sees a card or account number. A bank debit carries the account holder’s ACH authorization, or
for a Canadian account their pre-authorized debit (PAD) agreement, shown by Payra Elements next to the
secure fields and accepted there.
Orders are charged through the API from your server with your secret key, refunds are issued from the
order screen, and signed webhooks keep the order in step with the payment. Cards are
charged in USD or CAD, US bank accounts in USD and Canadian bank accounts in CAD, following
the store currency; each option hides itself on any other currency, so a store that turns on both bank
options offers the one that matches the order’s currency. A Test mode switch runs the whole flow on your sandbox keys,
so you can try the checkout without moving money.
Requirements
- WordPress 6.5 or newer, WooCommerce 8.3 or newer (HPOS and Checkout block compatible), PHP 8.1 or newer. The plugin is tested up to WooCommerce 11.1.
- A site served over https on a publicly reachable host. Payra only delivers webhooks to an
httpsURL on a public host, and the plugin only accepts anhttps://API host. - A Payra account with the API enabled, and a workspace to create keys in. Sandbox and live are separate workspaces with separate keys; see Environments.
- For US bank accounts, an ACH processor on that workspace. Without one the bank option cannot charge
(
payment_provider_not_configured). - For Canadian bank accounts (from version 1.4.0), Canadian pre-authorized debits set up on that workspace. Without them the option shows Bank account payments are temporarily unavailable and cannot be used.
Install
The plugin is on WordPress.org: Payra for WooCommerce.1
Install the plugin
In WordPress, open Plugins → Add New Plugin, search for Payra for WooCommerce (by Payra),
select Install Now and then Activate. WooCommerce must be installed and active first;
WordPress will not activate the plugin without it.To install from a file instead, download the zip from the plugin’s page on WordPress.org and use
Plugins → Add New Plugin → Upload Plugin.
2
Enable the gateway
Open WooCommerce → Settings → Payments. Payra appears as one entry; open it with
Manage (Finish set up on the older Payments table, until it is enabled). All four payment options are set up on that page. The plugin also
adds a Settings link on the Plugins screen that opens the same page.
Configure
The settings page has six parts: the keys every option shares, the card option, the US bank account option, the Canadian bank account option, the Payra checkout page, and the appearance of the fields.
An Advanced → API host field appears only on a store Payra pointed at another host; leave it alone unless Payra tells you otherwise.
Each API key field only accepts a key of its own kind (
pk_test_, sk_test_, pk_live_, sk_live_): a
value with the wrong prefix is rejected with must start with … and the previous value is kept, so a live
key cannot end up behind Test mode. The two webhook secret fields accept any whsec_ value, so check
that the sandbox one holds the sandbox endpoint’s secret (whsec_test_…) and the live one the live
endpoint’s (whsec_live_…). With Test mode on, the sandbox keys are used; off, the live keys. Each option is only
offered at checkout when it is switched on and both keys of the active mode are set, and the Payra
checkout page only on a store served over https. The options are independent and each is its own
entry at checkout. Offering the checkout page next to the card fields gives the customer two ways to pay
by card, so most stores pick one: the fields in the checkout, or the page.
1
Create the keys
In the Payra dashboard, open Settings → Integrations → API keys and create a publishable key
and a secret key in the workspace you are connecting, keeping the Payments and Payment
methods scopes on the secret key (every scope is selected by default). Paste them in the fields
of the matching mode. Keep Test mode on while you try the checkout with sandbox keys.
2
Register the webhook
In Settings → Integrations → Webhooks, add an endpoint pointing at your store’s webhook URL
(see below) and paste the
whsec_ secret it gives you in Sandbox webhook secret or Live
webhook secret, depending on the workspace you registered it on. The settings page prints the
URL of your store next to both fields.3
Go live
Repeat both steps on the live workspace, paste the live keys and the live webhook secret, and
turn Test mode off.
Appearance
From version 1.1.0, the Appearance section of the settings page sets how the card and bank fields look, so they match your store without any CSS. A preview beside the options follows each change, and nothing reaches your checkout until you select Save changes.
A color is picked from the swatch or typed as a hex code, and a size is dragged on the slider or typed
as a number. Reset to the Payra defaults puts every option back; save to apply it.
The options apply to the card fields and the bank account fields, on the classic checkout, the
Checkout block and My account → Payment methods → Add payment method. A store that leaves the
section as it is keeps the default look. The Payra checkout page is not affected: it carries your
business name and logo instead.
The font is not an option. The fields are rendered inside secure frames that a web font loaded by
your theme does not reach, so the text uses a font of the customer’s device.
Updates
New versions arrive through WordPress like any other plugin’s: Plugins shows the update when there is one. To have them installed for you, select Enable auto-updates on the Payra row of Plugins → Installed Plugins.Webhooks
The plugin exposes one webhook endpoint:["*"]: every event, including types added later), or at least
every payment., refund. and checkout_session. type. The plugin acts on payment.* events by
moving the order, settles an order paid on the Payra checkout page on checkout_session.completed, and
notes the order when a refund’s status changes (refund.*).
When a delivery arrives, the plugin:
- Verifies the signature against the secret of the event’s lane:
livemode: falseis checked with the sandbox webhook secret,livemode: truewith the live one. A missing secret or a bad signature answers400and changes nothing. A delivery signed more than five minutes ago is refused. - Refuses a lane mismatch. The ids in the event’s object (
pay_test_…/pay_live_…,re_test_…/re_live_…) must agree with the event’slivemode; otherwise it answers400lane_mismatch. A sandbox secret can never move an order paid live. - Finds the order by the
pay_id stored at the charge (arefund.*event carries its payment’s id), or by thecs_id for acheckout_session.*event. A payment the store did not make is acknowledged with200so Payra stops retrying, and nothing changes; so is apayment.failedfor an order sent to the checkout page, where the customer can try again, or for an order already paid. - Deduplicates on the event id. Each event id is remembered on the order it touched (the last
50); a retry or a resend of the same event is acknowledged with
200and ignored.
payment.* event, the plugin asks the API for the payment’s current status
(GET /payments/{id}) before moving the order, since deliveries can arrive out of order; the
event’s own snapshot is used only when that read does not succeed.
A
payment.* event for a payment the store asked for but never recorded (the API answered after
the plugin’s timeout, or the delivery beat the store) is matched by the order number sent as
reference, only when that number is also the order’s id (WooCommerce’s default numbering) and the
order is a Payra order for exactly that amount and currency. The payment id is then stored on the order so the money is accounted for. If the order was already
paid by another payment, an order note names both ids and asks you to refund one of them in Payra.How a payment flows
- When the customer picks an option, the browser asks your store for a payment method
session:
card, orus_bank_account(USD) oracss_debit(CAD) with the order total as theamountthe account holder will authorize. The store creates it with the secret key and hands back theclient_secret; the secret key never leaves PHP. Session requests are tied to a checkout in progress (a cart, or an order being paid) and limited to 30 per IP address, counted until 10 minutes pass without a new session. - Payra Elements mounts the secure fields of that option. For a bank account: routing and account number, account type (checking or savings), account holder type (individual or company), and the ACH authorization naming your business and the order total, which the customer must tick. For a Canadian bank account: institution number (3 digits), transit number (5 digits) and account number, the same two choices, and the pre-authorized debit agreement, which the customer must tick; no postal code is sent. If the total changes after the fields load (a coupon, shipping), a new session is created for the new total. On Place order, the session is confirmed in the browser with the customer’s name and billing postal code, and its id is sent to the store.
- The store charges the session in one call (
POST /paymentswithpayment_method_session) for the order total, with the order number asreferenceand Store name order #123 asdescription. When a logged-in customer ticks Save payment information, the charge also sendssave_payment_method: true; the card is added to their saved cards once Payra reads it back as saved. A card they already saved is not added again, and takes the new expiry if they typed one. - The order moves with the payment’s status, at the charge and again on every
payment.*event:
3-D Secure. When the card’s bank wants to verify the buyer, the charge answers
requires_action and the buyer
goes to Payra’s verification page (or the bank’s), then back to the order confirmation. The order stays Pending
payment meanwhile, and then follows the payment as above: if the bank refuses, the order turns Failed and the
buyer can pay it again. This needs the store on https (or localhost in test mode): elsewhere the plugin cannot
send the buyer back, so 3-D Secure cannot run at checkout.
For a bank order the thank-you page says: Thank you. Your bank debit is on its way, and we will email
you once your payment is confirmed.
Declines. A 402 from the charge (POST /payments) fails the order and tells the buyer why, in
a sentence written by the plugin for the decline code (for a card: insufficient_funds, invalid_payment_method,
processing_error, or a generic Your card was declined; for a bank account: The bank account
details were not accepted, Your payment could not be processed right now, or The debit was
refused). The decline code and the payment id go in an
order note. If a bank order’s total no longer matches the amount the customer authorized, the charge
is refused and the customer is asked to enter the bank details again. A refused charge spends the session: the checkout mounts a fresh one for the next
attempt. Any other error fails the order with a generic message for the buyer and the API’s detail
in an order note.
Idempotency. The charge is keyed per store, order and confirmed session (or saved card): the same
session submitted twice replays one charge (or one decline), while a new session, even with the same
card, is a new key. A
transport failure is retried once under the same key, so a charge the API completed without the
store hearing it comes back instead of being made again.
Payra checkout page
Pay with Payra shows no fields, and its button reads Continue to payment. It creates a checkout session for the order total (reference is the order number, success_url
the order confirmation, cancel_url the checkout, or the order’s payment page when the customer is paying
an existing order) and sends the customer to its url; an order note keeps the cs_… id. The order stays
Pending payment until the payment is confirmed, by whichever arrives first: the
checkout_session.completed webhook (found by the session, whatever your order numbers look like), the
payment.* webhook (found by the order number), or the customer’s return, when the order confirmation page
reads the session itself. The order then moves as in the table above; a bank debit paid on the page puts
it On hold. The page takes a US bank account on a USD order and a Canadian bank account on a CAD
order, each when its On the page box is ticked. If the workspace is not set up for Canadian bank
accounts, a CAD page opens for cards only and an order note says so. The payment is only applied when it paid exactly the order’s total; otherwise the order is
left as it is with a note to review it.
A declined card on the page leaves the order pending while the customer tries again there. The page is
closed when the customer places the order again (the checkout reuses the same pending order while the
cart is unchanged, and a new page opens), when the order is paid with the card or bank fields in the
checkout instead, and when the order is cancelled, by you or by WooCommerce once its unpaid hold runs
out. An abandoned page expires after 24 hours.
Refunds
The gateway declares refund support, so WooCommerce’s refund controls on the order screen offer to refund through Payra. A refund issued that way is sent toPOST /refunds for the payment stored on
the order, with the amount entered and the reason, truncated to 200 characters. Partial refunds are allowed until the payment is refunded in full, one at a time: while a refund is pending, another is refused (refund_in_flight). On success the
order gets the note Refunded … through Payra (refund re_…, status); when the API refuses, the
refund is rejected in WooCommerce with Payra’s error message and code, and no refund record is
kept.
Refund on the day of the payment. A card order refunded before the processor’s nightly batch is
accepted as a same-day card refund. A full refund voids the payment:
the note reads Refunded … through Payra (refund re_…, succeeded), followed by Payra payment pay_…
was voided, and the order stays Refunded. A partial refund gets the note
Refunded … through Payra (refund re_…, pending), and Payra sends the refund right after the batch.
When it goes through, the order gets Payra refund re_… for … went through; if the processor refuses
it then, Payra refund re_… for … failed (…): the money was not returned, and the WooCommerce refund
record stays, for you to review. While it waits, a second refund on the same order is refused
(refund_in_flight). A bank
order still on hold is refused before reaching the processor: Payra: this bank debit has not
settled yet. Refund it once the order is marked as paid.
The refund is keyed on the store, the order, the amount, the number of refunds already on the order
and the number of refunds Payra has refused on it, and retried once on a transport failure: a retry
after a timeout replays the refund the API already made instead of making a second one, while an
attempt after a refusal is a new request that reaches the processor again.
An order with no Payra payment id stored on it cannot be refunded through Payra: This order has no
Payra payment to refund.
A refund made in the Payra dashboard on a payment made by the plugin is announced by a
refund.* event. The plugin records it as an order note (Payra refund re_… for $25.00 went
through) but does not create a WooCommerce refund record or change the order status. Record it in
WooCommerce yourself if you need the order to reflect it.Pay-for-order page
Payra also works on the pay-for-order page (an existing order paid from My account). There, the order stands in for the cart and the billing fields: its currency decides which options are offered, its total is the amount a bank account authorizes, and its billing name and postal code are sent with the card or bank account. The order only stands in for the cart when the page carries the order’s key (thekey query parameter WooCommerce puts in the link) and the order still needs payment; otherwise
the plugin reads nothing from the order.
Troubleshooting
The fields say Loading secure card fields… (or Loading secure bank account fields…) and never load, or show Card payments are temporarily unavailable (or Bank account payments are temporarily unavailable). The plugin loads Payra Elements from the API host (<API host>/v1/payra.js). The
buyer sees that message when the script cannot load, when the publishable key is badly formatted, or
when the store cannot create a session with the secret key. A publishable key that looks right but
that Payra refuses shows Payra’s own error under the fields instead. Check that both keys of the
active mode are correct and that your server can reach the API host; a failed session creation is
logged under the payra source in WooCommerce → Status → Logs. A visitor over the session limit
sees Too many attempts. Please wait a few minutes and try again. on the Checkout block, and the
temporarily unavailable message on the classic checkout.
An option is not offered at checkout. Each option hides itself when it is switched off (Enable
Payra for cards, Bank accounts for US bank accounts, Canadian bank accounts for Canadian ones), when either key of the active mode is empty,
or when the currency is not one it charges (USD or CAD for cards, USD for US bank accounts, CAD for
Canadian bank accounts). On the
pay-for-order page the order’s currency is what counts, not the store’s.
A bank order stays on hold. That is normal until the debit settles.
If it never moves, check that the webhook is registered and its secret pasted (see
Webhooks); the order moves on payment.succeeded. In test mode a Canadian (EFT) order can stay on
hold, since the sandbox processor may not report the debit as settled.
A Test mode line appears above the fields. That is expected while Test mode is on. Above
the card fields: Test mode: use a test card such as 4111 1111 1111 1111 with expiration date 03/2030
and security code 737. Above the bank fields: Test mode: use a test routing number such as
021000021 and any account number of 4 to 17 digits. Above the Canadian bank fields: Test mode: no
money moves. Enter a 3-digit institution number, a 5-digit transit number and an account number of 7
to 12 digits. Turn Test mode off once the live keys are in
place.
A webhook answers 400. invalid_signature means the secret of that lane is missing or does
not match the endpoint that sent the event, or the signature is more than five minutes old; lane_mismatch means the event’s livemode and its
object’s ids disagree; not_an_event means the body is not a Payra event. 503 gateway_unavailable means WooCommerce is loaded but the Payra gateway is not. Without WooCommerce
active the route does not exist, and WordPress answers 404 rest_no_route.