Skip to main content
Payra for WooCommerce adds up to four payment options to your checkout, on both the classic [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 https URL on a public host, and the plugin only accepts an https:// 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.
Deleting the plugin removes its settings, including the stored secret keys. Deactivating it does not.

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.
The Appearance section of the plugin's settings, with its options and the preview
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:
Register it on each workspace you use (sandbox and live), and keep every event selected (the default in Add webhook endpoint, sent as ["*"]: 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.*).
If you offer bank accounts (US or Canadian), the webhook is required: a bank debit settles after checkout, and payment.succeeded is what moves the order from On hold to paid. Without the webhook, bank orders stay on hold.
When a delivery arrives, the plugin:
  • Verifies the signature against the secret of the event’s lane: livemode: false is checked with the sandbox webhook secret, livemode: true with the live one. A missing secret or a bad signature answers 400 and 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’s livemode; otherwise it answers 400 lane_mismatch. A sandbox secret can never move an order paid live.
  • Finds the order by the pay_ id stored at the charge (a refund.* event carries its payment’s id), or by the cs_ id for a checkout_session.* event. A payment the store did not make is acknowledged with 200 so Payra stops retrying, and nothing changes; so is a payment.failed for 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 200 and ignored.
For a 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

  1. When the customer picks an option, the browser asks your store for a payment method session: card, or us_bank_account (USD) or acss_debit (CAD) with the order total as the amount the account holder will authorize. The store creates it with the secret key and hands back the client_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.
  2. 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.
  3. The store charges the session in one call (POST /payments with payment_method_session) for the order total, with the order number as reference and Store name order #123 as description. When a logged-in customer ticks Save payment information, the charge also sends save_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.
  4. 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.
If the API does not answer the charge at all, even on the retry, the order is set to Failed with the note Payra did not answer the charge (<error>). Its outcome is unknown: check Payra for a payment with reference <order number> before charging again. The buyer is asked to contact the store before trying again. Look the reference up in Payra before charging the customer a second time; if the payment did go through, the payment.succeeded webhook also matches the order by that reference (when the order number is the order’s id).

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 to POST /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 (the key 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.