A WooCommerce payment gateway that bridges your store to Grin (GRIN, MimbleWimble) through the Such-Software BTCPay Grin plugin.
Works with the classic and block (Cart/Checkout block) checkout, and is HPOS-compatible.
This plugin is only the WooCommerce half. It does not run a Grin wallet or node itself; it talks to a BTCPay Server that does. You need all of the following already working:
- A BTCPay Server you control (a base URL like
https://btcpay.example.com). - The Such-Software BTCPay Grin plugin
installed on that BTCPay Server, with:
- a store that has Grin enabled, and
- a synced grin-wallet + grin node behind it (so invoices can be issued and watched).
- A WooCommerce site (WooCommerce 6.0+, PHP 7.4+) where you can install this plugin.
- The ability to reach your WooCommerce webhook endpoint from BTCPay — if a bot wall or page
cache sits in front of WordPress, the path
/wp-json/grin-btcpay/v1/webhookmust be exempted (see Configuration).
If you don't have the BTCPay Grin plugin and a synced wallet/node yet, set that up first — this gateway has nothing to talk to without it.
The BTCPay Grin plugin is not a native BTCPay payment method (it has no
IPaymentMethodHandler), so Grin never appears on a standard BTCPay invoice. It is a
self-contained flow with its own REST API, slatepack checkout page, and HMAC-signed webhooks
(originally built for headless Medusa storefronts). The official BTCPay for WooCommerce
plugin therefore can't surface Grin. This plugin bridges WooCommerce to that REST API.
WooCommerce checkout ─ customer picks "Grin (GRIN)"
│
▼ process_payment()
POST {BTCPay}/stores/{storeId}/plugins/grin/invoices
{ amount, currency, orderId, redirectUrl }
◄── { invoiceId, checkoutUrl, amount(GRIN) }
│ order → on-hold (awaiting payment), redirect customer
▼
Grin slatepack checkout (on BTCPay) ─ pay via slatepack ─► on broadcast, browser returns
│
▼ HMAC-signed webhook → POST /wp-json/grin-btcpay/v1/webhook
order advances (see status mapping)
The fiat→GRIN conversion happens inside BTCPay (Gate.io rate); the GRIN amount is fixed by the issued invoice, so the customer can only pay the exact requested amount.
| BTCPay event | WooCommerce result |
|---|---|
| invoice created | on-hold — awaiting payment (reserves stock) |
InvoiceProcessing |
stays on-hold — payment broadcast, awaiting confirmations |
InvoicePaymentSettled |
payment_complete() → processing (or completed for virtual) |
InvoiceInvalid |
failed — reorg / double-spend after confirmation |
InvoiceExpired |
cancelled — window elapsed unpaid |
This matches BTCPay's recommended convention (settle → "defaults", letting WooCommerce choose
processing vs. completed by product type). Orders start on-hold (not pending) so
WooCommerce's default unpaid-order auto-cancel doesn't kill slow crypto payments before the
window is up.
Webhook payload note: the Grin plugin sends two payload shapes — the checkout (broadcast) path uses
metadata.order_id; the monitor path (settled/invalid/expired) usesmetadata.medusa_cart_idand omits the top-levelinvoiceId. This plugin reads both, which is required for confirmations and expiry to register.
Primary: the BTCPay InvoiceExpired webhook → order cancelled. Fallback: a WooCommerce
scheduled check (Payment window (minutes), default 1440 / 24h to match BTCPay's invoice
expiry) cancels the order if it's still unpaid and the webhook was missed.
- WooCommerce 6.0+ (tested on 10.8.1, block checkout + HPOS).
- PHP 7.4+.
- A BTCPay Server running the Such-Software Grin plugin, with a store, Grin enabled, and a synced grin-wallet + node.
- Copy this folder to
wp-content/plugins/grin-btcpay-woocommerce/. - Activate Grin (BTCPay) for WooCommerce.
- Configure (below).
WooCommerce → Settings → Payments → Grin (GRIN):
| Field | Value |
|---|---|
| Enable | on |
| Title | e.g. Grin (GRIN) |
| BTCPay URL | e.g. https://btcpay.example.com |
| BTCPay Store ID | the store with the Grin plugin enabled |
| Webhook Secret | a shared secret (see below) |
| Payment window | minutes before an unpaid order is cancelled (default 1440) |
BTCPay → your store → Grin settings:
- Webhook URL:
https://YOURSITE/wp-json/grin-btcpay/v1/webhook - Webhook Secret: the same value as above.
If your site sits behind a bot wall (e.g. Anubis) or page cache, exempt the exact path
/wp-json/grin-btcpay/v1/webhook so the signed POST reaches WordPress directly (it is
HMAC-authenticated).
- Webhooks authenticated by HMAC-SHA256 over the raw body, constant-time compared
(
hash_equals) againstbtcpay-sig: sha256=<hex>. Missing/invalid signature →401; no secret configured → endpoint refuses (500). - Invoice binding: each webhook's invoice id is checked (constant-time) against the invoice id stored on the order, so a valid signature for one order can't be replayed against another.
- Idempotent:
payment_complete()only runs when the order isn't already paid. - Amount integrity is enforced upstream — the GRIN amount is locked into the issued invoice.
- The webhook route is intentionally
permission_callback => __return_true(the HMAC signature is the auth, not a WP nonce — the caller is BTCPay, not a browser). The secret is never logged.
BTCPay-side hardening (outside this plugin): the
…/plugins/grin/invoicescreate endpoint is unauthenticated by design. If invoice spam is a concern, restrict that path at your reverse proxy to your store server. Keep thecheckout/statusGETs public.
Enable Debug logging in the gateway settings, then watch WooCommerce → Status → Logs
(source grin_btcpay). It logs invoice-create requests and incoming webhooks (never secrets).
GPL-2.0-or-later.