Plaid Payments is a WooCommerce payment gateway for one-time US bank payments through Plaid Transfer UI.
The customer chooses Pay by Bank, connects a bank through Plaid Link, selects an account and authorizes an ACH debit. Plaid Payments keeps the server authoritative, follows the payment through the Plaid Transfer lifecycle, reconciles missed events, projects the result into WooCommerce, supports native refunds, and continues monitoring for ACH returns after the order is paid.
This repository contains the complete human-readable PHP, TypeScript and SCSS source plus the build and test tooling used to produce the WordPress.org release ZIP.
- Plaid Transfer UI for one-time Pay by Bank checkout.
- Standard ACH and Same Day ACH.
- WooCommerce Classic Checkout and Checkout Blocks.
- HPOS compatible using WooCommerce CRUD APIs.
- Server-authoritative payment confirmation: browser callbacks never mark an order paid.
- Verified Plaid webhooks using ES256 JWT verification and exact request-body hashing.
- Durable transfer event ingestion with account-scoped cursors and idempotent event processing.
- Background reconciliation for missed webhooks, stale payments and open refunds.
- Native WooCommerce refunds with full and multiple partial refunds.
- ACH return handling with order state projection, notes, alerts and merchant email.
- Duplicate-payment protection using durable reservations and database mutexes.
- Payment attempt history for failed attempts without confusing late events with a newer attempt.
- Plaid account-change guard while Production payments or refunds are still being monitored.
- Configuration health, connection testing, diagnostics, Site Health and structured redacted logs.
- WP-CLI operations for status, event sync, reconciliation, order sync and Sandbox testing.
- Reproducible release packaging with a strict runtime allowlist.
| Component | Requirement |
|---|---|
| WordPress | 6.6+ |
| WooCommerce | 8.7+ |
| PHP | 8.1–8.4 |
| PHP extension | OpenSSL |
| Database | MySQL 8.0+ or MariaDB 10.11+ |
| Currency | USD |
| Plaid | Plaid Transfer enabled |
| Production | HTTPS |
The plugin declares compatibility with WooCommerce custom order tables (HPOS) and cart/checkout blocks.
Install the release ZIP as a normal WordPress plugin and activate it with WooCommerce active.
Open:
WooCommerce → Settings → Payments → Plaid Payments
In the Plaid Dashboard open:
Developers → Keys
Start with Sandbox and enter:
- Client ID
- Sandbox Secret
Plaid Payments never renders the stored Secret back into the settings field. Saving an empty Secret preserves the existing value; removing it is an explicit action.
In the same Plaid environment create and publish a Link customization with:
Account Select = Enabled for one account
Use the same language as the WooCommerce store and enter the customization name in Plaid Payments.
The Link customization is required in both Sandbox and Production.
For the normal Plaid Ledger configuration, leave Funding Account ID empty.
Only Plaid Transfer accounts without Ledger should provide a Funding Account ID.
Plaid Payments displays the site-specific endpoint:
https://example.com/wp-json/buckmerce-plaid/v1/webhook
Add it in the Plaid Dashboard as a Transfer event webhook for the selected environment.
Use Test connection. The settings screen evaluates the local configuration and records the Plaid connectivity result.
When the status is ready, enable:
Offer Pay by Bank at checkout
Test the complete flow in Sandbox before switching to Production.
Customer
│
│ chooses Pay by Bank
▼
WooCommerce
│
│ process_payment()
▼
Plaid Payments payment reservation
│
│ /transfer/intent/create
▼
Plaid Transfer Intent
│
│ /link/token/create
▼
Plaid Link / Transfer UI
│
│ customer connects bank and authorizes
▼
Plaid Payments /complete
│
│ /transfer/intent/get
│ /transfer/get
▼
WooCommerce order → On hold
│
│ verified webhook + /transfer/event/sync
▼
pending → posted → settled → funds_available
│
└─ failed / cancelled / returned
The browser is intentionally not authoritative. A successful Plaid Link callback means the UI flow completed; it does not mean the WooCommerce order is paid.
Plaid Payments re-reads the Transfer Intent and Transfer from Plaid and verifies the server-side snapshot before projecting state into WooCommerce.
For each new payment attempt Plaid Payments creates an immutable server-side snapshot containing the authoritative order data, including:
- WooCommerce order ID
- amount
- currency
- Plaid environment
- attempt identity
The Transfer Intent request uses:
mode = PAYMENT
iso_currency_code = USD
ach_class = web
network = same-day-ach | ach
The customer legal name is derived from WooCommerce billing first and last name and is required. A valid billing email is included when available.
Application metadata is limited to non-secret correlation values.
Payment creation is protected by:
- a plugin-owned payment reservation table;
- a database advisory mutex;
- immutable attempt snapshots;
- compare-and-set transfer binding;
- idempotent completion behavior.
Parallel tabs, repeated completion requests and double clicks should not create multiple active transfers for one order.
The gateway participates as a normal WC_Payment_Gateway.
Before payment begins, Plaid Payments validates that the billing first and last name can represent the bank account holder's legal name.
Plaid Payments registers a WooCommerce Blocks payment method and uses the same server-side gateway implementation and order lifecycle.
The plugin declares HPOS compatibility and uses WooCommerce CRUD APIs for order state and order metadata. It does not depend on direct wp_posts / wp_postmeta access for payment state.
After checkout, the customer is sent to WooCommerce's native order-pay flow. The page displays server-derived order information and loads Plaid Link only when the order can be paid.
Customer payment REST calls are protected by:
- order ID;
- WooCommerce order key;
- ownership/session access;
- a payment nonce.
A numeric order ID alone is never sufficient.
| Setting | Purpose |
|---|---|
| Offer Pay by Bank at checkout | Enables new payments. Existing payments/refunds continue to be maintained when disabled. |
| Title | Checkout payment-method title. |
| Description | Checkout payment-method description. |
| Environment | Sandbox or Production. |
| Client ID | Plaid account Client ID. |
| Secret | Environment-specific Plaid Secret. |
| Funding Account ID | Optional for Plaid Transfer accounts without Ledger. |
| Link customization name | Required published Transfer UI customization. |
| Bank statement description | Stable statement descriptor, maximum 10 letters/digits/spaces. |
| Payment network | Same Day ACH or Standard ACH. |
| Mark order paid when | Funds available (default) or settled. |
| Debug logging | Enables debug/info entries in WooCommerce logs. |
| Uninstall cleanup | Removes plugin settings/tables when the plugin is deleted; order audit data remains. |
Production payments are unavailable unless the site uses HTTPS. New payments are also unavailable when the order currency is not USD.
Internal payment states include:
new
intent_creating
intent_created
intent_pending
intent_failed
intent_uncertain
transfer_created
pending
posted
settled
funds_available
failed
cancelled
returned
manual_review
Transitions are explicitly validated. Unknown, stale or conflicting events are not allowed to silently regress payment state.
Typical successful flow:
Pending payment
→ On hold
→ payment_complete()
→ Processing or Completed
The final Processing/Completed choice remains WooCommerce's normal payment_complete() behavior.
The merchant can choose whether payment_complete() occurs when Plaid reports:
- Funds available — default and recommended by the plugin; or
- Settled.
Plaid Payments continues monitoring completed bank payments through the applicable return-monitoring period.
When a payment is returned, the plugin records the return information and surfaces it operationally:
- payment state becomes
returned; - WooCommerce order is moved to Failed when appropriate;
- private order note is added;
- orders-list status indicator is updated;
- persistent merchant alert is created;
- merchant email is sent;
- the original payment and attempt history remain available.
A returned payment is not automatically debited again through Transfer UI.
Plaid has specific retry semantics for certain ACH return codes when using direct transfer creation. The 1.0 Transfer UI flow does not express those retry markers, so Plaid Payments refuses a new bank debit for the returned order instead of risking an invalid reprocessing attempt.
Payments that failed before money moved can start a fresh payment attempt.
Plaid Payments implements WooCommerce automatic refunds through Plaid Transfer.
Supported behavior includes:
- full refunds;
- multiple partial refunds;
- deterministic idempotency keys;
- refund reservations;
- refund lifecycle tracking;
- reconciliation of uncertain refund creation;
- refund cancellation when Plaid still allows it;
- merchant alerts for failed or returned refunds;
- detection of refunds created outside WooCommerce.
The plugin deliberately applies a stricter merchant policy than Plaid's minimum API eligibility: it waits for the debit to reach an accepted settled state before offering a normal WooCommerce refund path.
Refund state is maintained independently from the order payment state.
Typical refund lifecycle:
creating
→ pending
→ posted
→ settled
or
creating / pending / posted
→ failed
→ cancelled
→ returned
→ uncertain
Endpoint:
POST /wp-json/buckmerce-plaid/v1/webhook
The endpoint is reachable without a WordPress login because Plaid is the caller, but it is not unauthenticated.
Authorization runs in the REST permission_callback and verifies the Plaid request before business processing.
Verification includes:
- request body size limit;
Plaid-VerificationJWT presence and shape;alg = ES256;- validated
kid; - Plaid verification key fetched through
/webhook_verification_key/get; - ES256 signature verification using the bundled, namespace-prefixed
firebase/php-jwt; - issued-at freshness;
- SHA-256 of the exact raw request body;
- constant-time comparison with
request_body_sha256.
Verified payloads are request-scoped and consumed once by the webhook handler.
A valid TRANSFER_EVENTS_UPDATE webhook does not directly modify an order. It schedules transfer-event synchronization; authoritative state comes from Plaid's event stream and transfer reads.
Namespace:
buckmerce-plaid/v1
| Route | Method | Purpose | Authorization |
|---|---|---|---|
/link-token |
POST | Issue a Plaid Link token for the current payment attempt | Order access + payment nonce |
/complete |
POST | Re-read the Transfer Intent after Link finishes | Order access + payment nonce |
/webhook |
POST | Receive Plaid Transfer event notifications | Plaid JWT + request body verification |
The customer-facing REST endpoints also have bounded per-order rate limiting.
The runtime client uses an explicit endpoint allowlist.
/transfer/intent/create
/transfer/intent/get
/link/token/create
/transfer/get
/transfer/cancel
/transfer/event/sync
/transfer/refund/create
/transfer/refund/get
/transfer/refund/cancel
/transfer/configuration/get
/transfer/ledger/get
/webhook_verification_key/get
/sandbox/transfer/simulate
/sandbox/transfer/refund/simulate
/sandbox/transfer/fire_webhook
The Plaid client refuses Sandbox-only endpoints in Production.
All Plaid API requests use JSON over HTTPS, certificate verification, a 30-second timeout, no redirects and the pinned Plaid API version:
2020-09-14
Plaid request_id values are retained for support correlation.
Plaid Payments uses WooCommerce Action Scheduler.
Action group:
buckmerce-plaid
Primary hooks:
buckmerce_plaid_transfer_event_sync
buckmerce_plaid_reconcile
buckmerce_plaid_reconcile_continue
Verified webhooks enqueue a unique event-sync action. Duplicate notifications coalesce when an event-sync job is already scheduled.
The event sync reads Plaid /transfer/event/sync with an account-scoped cursor and stores the events before applying them.
A recurring reconciliation pass runs every 15 minutes while there is actual operational work:
- monitored payments;
- open refunds;
- event backlog; or
- a failed event sync that still needs retrying.
A pass can also schedule a bounded continuation one minute later when more work remains.
Disabling the payment gateway does not abandon existing payment maintenance. Once no payment/refund/event work remains, recurring reconciliation can stop.
Plaid Payments owns three operational tables:
{prefix}buckmerce_plaid_events
{prefix}buckmerce_plaid_payment_locks
{prefix}buckmerce_plaid_refunds
Stores durable Plaid transfer/refund events with:
- environment;
- Plaid account fingerprint;
- event ID;
- event type;
- transfer/order correlation;
- processing state;
- lease/attempt metadata.
Event identity is unique per environment + Plaid account + event ID.
One row per WooCommerce order coordinates:
- current attempt;
- Transfer Intent ID;
- Transfer ID;
- payment state;
- account identity;
- reconciliation scheduling;
- monitor-until date;
- reservation ownership.
Tracks:
- WooCommerce refund;
- Plaid refund;
- amount/currency;
- environment/account;
- payment attempt;
- idempotency key;
- refund state;
- reconciliation metadata;
- failure and request IDs.
Order-level payment details remain in WooCommerce order metadata via WooCommerce CRUD.
Schema installation is forward-only and idempotent and verifies required columns/indexes before recording the schema version.
A Plaid Client ID/environment combination is part of the operational identity.
Event streams, cursors, payment epochs and refund identities are scoped so an event from one Plaid account cannot be mistaken for an event with the same numeric ID from another account.
Plaid Payments also refuses a dangerous Plaid account/environment switch while monitored Production work still depends on the old credentials.
This protects long-running ACH return and refund monitoring from accidental credential replacement.
The plugin is designed around fail-closed payment processing.
The browser cannot choose or override:
- order amount;
- currency;
- environment;
- Transfer Intent;
- funding account;
- payment state.
Plaid credentials:
- stay server-side;
- are not returned in settings HTML;
- are not sent to browser JavaScript;
- are recursively redacted from structured logs.
Sensitive key fragments include credentials, tokens, cookies, Plaid verification headers, bank account/routing fields, names, email, phone, postal/address data, JWT/signature fields and private/API keys.
Request/response bodies are not used as routine Plaid log payloads.
Webhook verification uses a maintained JWT library rather than custom cryptography.
Sensitive merchant actions require:
manage_woocommerce;- an action-specific nonce;
- server-side revalidation.
MySQL advisory locks and durable reservations protect creation and mutation paths that cannot safely race.
The payment settings screen reports states such as:
Ready
Disabled
Incomplete
Attention required
The checklist covers:
- Plaid credentials;
- environment;
- HTTPS;
- USD currency;
- Link customization;
- schema;
- webhook route;
- connection result;
- Action Scheduler/background processing.
The WooCommerce order panel can surface:
- current payment attempt;
- Transfer Intent and status;
- Transfer ID and status;
- created/settled/funds-available dates;
- expected funds date;
- return windows;
- returned/failure/manual-review information;
- last sync/event/request ID;
- refund history;
- previous payment attempts;
- payment/refund alerts.
Available merchant actions are exposed only when appropriate, including:
- Sync with Plaid;
- Cancel bank payment;
- manual-review decisions;
- allow another attempt after eligible failures.
Diagnostics provide a sanitized support view of:
- versions;
- HPOS/Blocks state;
- database server;
- configuration readiness;
- event cursor;
- background jobs;
- last webhook;
- last event sync;
- last reconciliation;
- payment/refund counts and backlog.
Site Health reports critical/recommended problems for broken configuration or stalled maintenance.
WooCommerce log source:
buckmerce-plaid
Warnings/errors are available for operational failures; debug/info entries are controlled by the Debug logging setting.
Plaid Payments registers:
wp buckmerce-plaid status
wp buckmerce-plaid test-connection
wp buckmerce-plaid sync-events
wp buckmerce-plaid reconcile
wp buckmerce-plaid sync-order <order-id>
wp buckmerce-plaid refunds <order-id>Sandbox-only tools:
wp buckmerce-plaid simulate-refund <refund-id> <refund.posted|refund.settled|refund.failed|refund.returned>
wp buckmerce-plaid fire-sandbox-webhook \
--webhook-url=https://public.example/wp-json/buckmerce-plaid/v1/webhookCLI output is designed not to expose Plaid secrets.
Useful emitted actions include:
buckmerce_plaid_transfer_intent_created
buckmerce_plaid_payment_state_changed
buckmerce_plaid_payment_confirmed
buckmerce_plaid_payment_failed
buckmerce_plaid_payment_cancelled
buckmerce_plaid_payment_manual_review
buckmerce_plaid_payment_returned
buckmerce_plaid_webhook_processed
buckmerce_plaid_refund_created
buckmerce_plaid_refund_failed
buckmerce_plaid_refund_returned
buckmerce_plaid_refund_cancelled
Merchant alert email recipient can be changed with:
buckmerce_plaid_alert_recipient
Install the development toolchain:
composer installRuntime third-party dependency:
firebase/php-jwt
It is namespace-prefixed into:
Buckmerce\Plaid\Vendor\
using Strauss to reduce dependency collisions with other WordPress plugins.
npm ciHuman-readable front-end sources:
resources/ts/
resources/scss/
resources/images/
Generated release assets (build output: ignored by Git, shipped only in the release ZIP, no subdirectories):
assets/*.css
assets/*.js
assets/*.svg
Build:
npm run buildTypecheck:
npm run typecheckJavaScript build/test gate:
npm testcomposer validate --strict
composer syntax
composer lint
composer stan
composer testThe repository includes unit, integration, browser/E2E, package, Plugin Check and real Plaid Sandbox test paths.
Build the WordPress release ZIP:
npm run plugin-zipThe packaging script starts from an empty staging directory and copies only explicitly allowed runtime files.
The final ZIP is checked for:
- one expected plugin root;
- required runtime files;
- allowlisted contents only;
- no development/test files;
- no local environment files;
- no source maps;
- no nested archives;
- no obvious credentials;
- source/package parity;
- fresh generated-asset parity;
- PHP syntax;
- identity/version consistency.
Release archives use normalized permissions, timestamps and entry ordering to support deterministic rebuilds.
The repository includes:
- PHPUnit unit tests;
- WordPress/WooCommerce integration scripts;
- MySQL and MariaDB integration paths;
- concurrency/idempotency tests;
- JavaScript unit tests;
- browser checkout smoke tests;
- Classic Checkout and Checkout Blocks flows;
- HPOS coverage;
- refund lifecycle coverage;
- webhook REST/JWT verification coverage;
- Sandbox transfer/refund simulation;
- real signed Plaid Sandbox webhook testing when credentials are available;
- WordPress Plugin Check against the built ZIP.
GitHub Actions runs the quality matrix for pushes and pull requests and builds the release artifact from the tested commit.
Every tool and platform version in that pipeline is pinned, so a red run means the commit itself is broken. Two checks depend on the day of the run instead: known security advisories (bash scripts/audit-dependencies.sh) and Plugin Check comparing Tested up to with the current WordPress release. They warn on pushes and pull requests and block a release. A scheduled workflow audits the locked dependencies of master every day, and Dependabot opens security-update pull requests.
Plaid Payments uses Plaid to perform bank-payment functionality.
Server-to-server data sent to Plaid can include:
- Plaid Client ID and Secret;
- order amount and currency;
- statement description;
- customer billing legal name;
- customer billing email;
- internal order/payment correlation metadata;
- refund amount;
- transfer/refund identifiers.
Plaid Link is loaded from:
https://cdn.plaid.com/link/v2/stable/link-initialize.js
Plaid API environments:
https://sandbox.plaid.com
https://production.plaid.com
Plaid Terms: https://plaid.com/legal/
Plaid Privacy Policy: https://plaid.com/legal/#end-user-privacy-policy
Plaid Payments itself does not require a license key, subscription, paid feature unlock, trial timer or usage quota.
Version 1.0 intentionally focuses on:
US consumer Pay by Bank
USD
one-time payments
Plaid Transfer UI
ACH debit
WooCommerce
Not included in 1.0:
- credit/debit card processing;
- WooCommerce Subscriptions recurring payments;
- saved bank accounts;
- non-USD payment rails;
- automatic re-debit of returned ACH payments.
Keeping these boundaries explicit reduces ambiguity in payment semantics and keeps the one-time Transfer UI flow predictable.
Plaid Payments is licensed under GPL-2.0-or-later.
The bundled firebase/php-jwt library is distributed under BSD-3-Clause.