Skip to main content
Payra for WooCommerce adds up to three payment options to your checkout, on both the classic [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 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 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.
Deleting the plugin removes its settings, including the stored secret keys. Deactivating it does not.

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 records refund.* events as order notes.
If you offer bank accounts, the webhook is required: a bank debit settles in the processor’s next batch 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 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. 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 turns the session into a payment method and charges it (POST /payments) for the order total, with the order number as reference and Store name order #123 as description.
  4. 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.
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 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 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 (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 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.