[woocommerce_checkout] shortcode and the Checkout block: card and US bank account (ACH) 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, 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 and bank accounts in USD, following the store currency; each option
hides itself on any other 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 bank accounts, an ACH processor on that workspace. Without one the bank option cannot charge
(
payment_provider_not_configured).
Install
The plugin is provided by Payra as a zip file.1
Upload the plugin
In WordPress, open Plugins → Add New Plugin → Upload Plugin, choose the zip and activate it.
WooCommerce must be installed and active first; WordPress will not activate the plugin without it.
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 three 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 four parts: the keys every option shares, the card option, the bank account option, and the Payra checkout page.
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.
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
records refund.* events as order notes.
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_accountwith 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. 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 turns the session into a payment method and charges it (
POST /payments) for the order total, with the order number asreferenceand Store name order #123 asdescription. - The order moves with the payment’s status, at the charge and again on every
payment.*event:
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. A card or bank account Payra cannot turn into a payment method fails the order with We
could not verify your card. Please try again. (We could not use this bank account. Please check the
details or try another account.) and the API’s detail 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 payment method, and each confirmed session
makes one payment method: 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 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 after the payment settles. A card order refunded right after the charge is usually
refused by the processor until the charge settles: WooCommerce shows Payra: the processor refused
this refund (…). A card payment can usually be refunded once it has settled, the next business day;
try again then. Payra keeps that attempt as a failed refund and sends refund.failed. 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.succeeded: refund re_…
for $25.00) 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 bank accounts), 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 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 in the processor’s next batch.
If it never moves, check that the webhook is registered and its secret pasted (see
Webhooks); the order moves on payment.succeeded.
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 any future expiration
date and any security code. Above the bank fields: Test mode: use a test routing number such as
021000021 and any account number of 4 to 17 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.