Payment flow
How it works
The plugin does the API integration for you. This is what it's doing on your behalf.
shopper picks "pay with crypto"
│
▼
┌──────────────────┐ creates an order ┌─────────────┐
│ WooCommerce │ ────────────────────▶ │ CPT Payment │
│ (the plugin) │ ◀─── payment_url ──── │ │
└──────────────────┘ └─────────────┘
│ │
│ redirects the shopper │ shopper pays
▼ │ in crypto
┌──────────────────┐ │
│ hosted checkout │ ◀─────────────────────────────┘
└──────────────────┘
│
│ back to your thank-you page (success_url)
▼
┌──────────────────┐ callback with the order ┌─────────────┐
│ WooCommerce │ ◀─── token + status ──────── │ CPT Payment │
│ ?wc-api=… │ └─────────────┘
└──────────────────┘
│
▼
order status moves: pending → on-hold → completedThe redirect is what the shopper sees. The callback is what actually moves the order, which is why it has to be reachable from the internet. If your site is behind a firewall or a "coming soon" plugin, orders will sit at pending forever even though the payment landed.
Dashboard setup
Create the store
One entry in the dashboard, which mints the key the plugin authenticates with.
- 1
Open Plugins and pick WooCommerce
Go to Plugins, select the WooCommerce card, then Add store.
- 2
Name it and give us your site URL
Nickname is for your own reference. Callback URL is your WordPress site root, e.g. https://yourstore.com.
- 3
Copy the API key
The store row shows an api_key. You paste it into WordPress in step 04.
| Field | What to enter | Notes |
|---|---|---|
| Nickname | Anything you'll recognise | Shown only in your dashboard. |
| Callback URL | https://yourstore.com | Must be HTTPS in production. HTTP is accepted only while the backend runs with DEBUG on. |
| api_key | Generated for you | The plugin's API Auth Token. Server-side secret, never in a theme or JS file. |
WordPress plugin
Install the plugin
Grab the zip, then pick whichever install route matches how you deploy WordPress.
WooCommerce plugin
CptPayment for WooCommerce
cptpayment-for-woocommerce-2.2.0.zip- Version 2.2.0
- 68 KB
- WordPress 5.8+
- WooCommerce 6.0+
- PHP 7.4+
Ready to upload as-is. The PHP SDK it needs is bundled, so there is no composer install step and no build to run.
# No terminal needed.
# 1. Download cptpayment-for-woocommerce.zip
# 2. WordPress admin -> Plugins -> Add New -> Upload Plugin
# 3. Choose the zip -> Install Now -> Activate# Fastest on a server you can SSH into.
wp plugin install ./cptpayment-for-woocommerce.zip --activate
# Confirm it registered as a gateway:
wp option get woocommerce_cptpayment_settings --format=json | jq# If you manage WordPress as a Composer project (Bedrock and friends).
composer require cptpayment/cptpayment-for-woocommerce
# The plugin bundles its own vendor/ with the PHP SDK it needs
# (cptpayment/cptpayment-php ^4.1), so nothing else to install.
wp plugin activate cptpayment-for-woocommercecd wp-content/plugins
git clone https://github.com/cptpayment/woocommerce-plugin.git cptpayment-for-woocommerce
cd cptpayment-for-woocommerce
composer install --no-dev # only if vendor/ is not committed in your checkout
wp plugin activate cptpayment-for-woocommerce| Requirement | Version |
|---|---|
| WordPress | 5.8 or newer |
| WooCommerce | 6.0 or newer, classic checkout or the Cart/Checkout blocks |
| PHP | 7.4 or newer |
| Plugin | 2.2.0 (bundles cptpayment/cptpayment-php ^4.1) |
Gateway settings
Configure the gateway
WooCommerce→Settings→Payments→Cryptocurrencies via CptPayment.
| Setting | Default | What it does |
|---|---|---|
| Enable CptPayment | off | Turn the gateway on. Nothing shows at checkout until you do. |
| API Auth Token | empty | Paste your api_key here. This is the one field you must fill in. |
| Title | Cryptocurrencies via CptPayment | The label the shopper sees in the payment method list. |
| Description | Pay with BTC, LTC, ETH, TRX… | Helper text under the label at checkout. |
| Crypto Payment Fee/Discount (%) | 0 | Adjusts the total for crypto orders. Negative values are a discount. |
| Pre-fill CptPayment invoice email | yes | Sends the billing email so the shopper gets payment-status mail. |
| Order status mapping | see section 06 | Which WooCommerce status each crypto status maps to. |
If you rebuild staging from scratch often, the same settings are one option row:
# Everything the settings screen writes, scriptable for staging rebuilds.
wp option patch update woocommerce_cptpayment_settings enabled yes
wp option patch update woocommerce_cptpayment_settings api_auth_token "$CPT_API_KEY"
wp option patch update woocommerce_cptpayment_settings title "Pay with crypto"
wp option patch update woocommerce_cptpayment_settings purchaser_email_status yes
wp option get woocommerce_cptpayment_settings --format=json | jq<?php
// Point the plugin at a non-production API. Undocumented in the settings UI:
// the bundled SDK reads this constant and otherwise defaults to
// https://api.cptpayment.com/plugins.
//
// This is the only way to test against staging without editing plugin files.
define('CPTPAYMENT_API_BASE', 'https://api-staging.cptpayment.com/plugins');
// While integrating, log the callbacks the plugin receives.
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);Before launch
Test a checkout
Do this before you announce it. Most setup problems only show on a real order.
- 1
Place a cheap real order
Add a $1 product, check out, and pick the crypto method. You should be redirected to our hosted checkout with the right amount and your store name on it.
- 2
Confirm the order exists on both sides
The WooCommerce order should be pending, and the same order should appear under your store's Orders in the dashboard. If it's missing here, the API key is wrong.
- 3
Pay it, and watch the status move
Once the payment is seen the order goes on-hold, then completed when it confirms. If it stays pending after payment, the callback isn't reaching you: go to section 10.
- 4
Let one expire
Start a checkout and walk away. After the payment window closes the order should become cancelled and the stock should return.
Order states
Status mapping
How a crypto payment status becomes a WooCommerce order status.
| Crypto status | WooCommerce status | Why |
|---|---|---|
| paid | completed | Confirmed and settled. This is the one to fulfil on. |
| awaiting | pending | Checkout opened, nobody has paid yet. |
| confirming | on-hold | Seen on chain, waiting for confirmations. Temporary. |
| underpaid | on-hold | Less than the total arrived. Needs a human. |
| overpaid | completed | They paid. Refund the surplus yourself. |
| expired | cancelled | Payment window closed unpaid. |
| canceled | cancelled | Shopper abandoned it. |
| refunded | refunded | Refund settled. |
| invalid | failed | Something was wrong with the payment. |
Every row is editable on the settings screen, and your saved choices are merged over these defaults, so callbacks map correctly even before you've ever pressed save. Section 09 shows how to change one in code.
Developer detail
How callbacks are trusted
Developer detail. This is where WooCommerce differs from the custom-store API.
We post the status change to https://yourstore.com/?wc-api=wc_gateway_cptpayment. The plugin has to prove the request is really from us before touching the order.
{
"order_id": "1042",
"status": "paid",
"price_amount": 49.99,
"price_currency": "USD",
"callback_token": "a7f3c1e9d2b84f60a1c5e8d7b3f92a04",
"event": "order.paid",
"event_id": "evt_01J8W2Q5N3XK",
"api_version": "2024-07-27"
}<?php
// WooCommerce auth is NOT the HMAC scheme custom stores use.
//
// On checkout the plugin stores the per-order token we return:
// $order->add_meta_data(self::ORDER_TOKEN_META_KEY, $gateway_order->token, true);
//
// On callback it compares that stored token against the one in the body:
private function is_token_valid(WC_Order $order, string $token) {
$order_token = $order->get_meta(static::ORDER_TOKEN_META_KEY);
// hash_equals, not ==, so a wrong token can't be recovered by timing.
return !empty($order_token) && hash_equals($order_token, $token);
}
// Then an amount guard, so a "paid" callback for less than the order total
// cannot complete the order:
private function is_order_paid_status_valid(WC_Order $order, $price) {
return $order->get_total() >= (float) $price;
}| WooCommerce | Custom store | |
|---|---|---|
| Secret | Per-order token, issued at checkout | One api_secret per store |
| Check | hash_equals against order meta | HMAC-SHA256 over canonical JSON |
| Scope of a leak | One order | Every webhook until you rotate |
| You implement it | No, the plugin does | Yes |
Development
Staging and local dev
The setting you'll want and won't find in the UI.
The bundled SDK defaults to https://api.cptpayment.com/plugins and the settings screen has no field to change it. Define a constant in wp-config.php instead:
<?php
// Point the plugin at a non-production API. Undocumented in the settings UI:
// the bundled SDK reads this constant and otherwise defaults to
// https://api.cptpayment.com/plugins.
//
// This is the only way to test against staging without editing plugin files.
define('CPTPAYMENT_API_BASE', 'https://api-staging.cptpayment.com/plugins');
// While integrating, log the callbacks the plugin receives.
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);cloudflared tunnel --url http://localhost:8080
# or: ngrok http 8080
# Then set BOTH:
# - the store's Callback URL in the CPT dashboard -> https://<tunnel-host>
# - WordPress site + home URL -> https://<tunnel-host>
wp option update siteurl "https://<tunnel-host>"
wp option update home "https://<tunnel-host>"Updating siteurl matters: WooCommerce builds the success and cancel URLs from it, so leaving it as localhost sends the shopper somewhere they can't reach after paying.
Custom behaviour
Extend it in code
The gateway drives ordinary WooCommerce transitions, so your existing hooks work.
<?php
// The settings screen exposes the CPT-status to WC-status mapping, and the
// saved values are merged OVER the defaults, so you only override what differs.
//
// Send confirming orders to processing instead of on-hold:
add_action('init', function () {
$settings = get_option('woocommerce_cptpayment_settings', []);
$settings['order_statuses'] = array_merge(
$settings['order_statuses'] ?? [],
['confirming' => 'wc-processing']
);
update_option('woocommerce_cptpayment_settings', $settings);
});<?php
// The gateway drives ordinary WooCommerce status transitions, so you don't need
// plugin-specific hooks. Use the WC ones you already know.
add_action('woocommerce_order_status_completed', function ($order_id) {
$order = wc_get_order($order_id);
if ($order->get_payment_method() !== 'cptpayment') {
return;
}
// Crypto payment settled. Grant access, ship, notify fulfilment.
my_fulfil($order);
}, 10, 1);
// Underpaid lands on-hold by default. Catch it and flag for review.
add_action('woocommerce_order_status_on-hold', function ($order_id) {
$order = wc_get_order($order_id);
if ($order->get_payment_method() !== 'cptpayment') {
return;
}
$order->add_order_note('Crypto payment needs review: underpaid or confirming.');
}, 10, 1);<?php
// The gateway has a built-in percentage fee or discount for paying in crypto
// (negative values discount). Set it from the settings screen, or in code:
add_action('init', function () {
$settings = get_option('woocommerce_cptpayment_settings', []);
$settings['mark_up'] = -2; // 2% off for paying in crypto
update_option('woocommerce_cptpayment_settings', $settings);
});Diagnose
Troubleshooting
# Is the gateway registered and on?
wp option get woocommerce_cptpayment_settings --format=json | jq '{enabled, api_auth_token: (.api_auth_token | length)}'
# Did a callback ever arrive? The plugin logs failures via error_log().
tail -f wp-content/debug.log | grep -i cptpayment
# Which token did checkout store for an order?
wp post meta get 1042 _cptpayment_order_token
# Is the callback URL we'd hit actually reachable from outside?
curl -sS -o /dev/null -w '%{http_code}\n' \
"https://yourstore.com/?wc-api=wc_gateway_cptpayment"
# 200 means WordPress answered. A 403/503 usually means a firewall,
# Cloudflare "Under Attack" mode, or a security plugin blocking POSTs.| Symptom | Cause | Fix |
|---|---|---|
| No crypto option at checkout | Gateway not enabled, or token empty | Enable it and paste the api_key. The gateway hides itself with no token. |
| Option missing only on block checkout | Plugin older than 2.0 | Update. Block support is registered separately from the classic gateway. |
| "Unable to start the CptPayment checkout" | Order creation was rejected | Wrong api_key, or the store was deleted in the dashboard. Check debug.log. |
| Order stuck at pending after a real payment | Callback never arrived | Curl the ?wc-api= URL from outside. Firewalls, Cloudflare bot-fight and security plugins block these POSTs. |
| Callback arrives, order doesn't move | Token mismatch | debug.log shows "callback token does not match". Usually the order was created against a different store or a different API base. |
| Paid orders land on-hold, never completed | Amount guard rejected it | The order total exceeds the amount paid. Check for a fee or currency-conversion difference. |
| Everything works on live, nothing on staging | Staging points at production API | Set CPTPAYMENT_API_BASE in wp-config.php, see section 08. |
| Site moved and orders broke | Stale Callback URL | Update the store's Callback URL in the dashboard to the new host. |
Choosing the right path
When to use the API instead
The plugin is the right answer for almost every WooCommerce store. Almost.
| You want | Use |
|---|---|
| Crypto at WooCommerce checkout, minimal work | This plugin |
| Shoppers to never leave your site during payment | Custom store, Mode B |
| A checkout that isn't WooCommerce at all | Custom store |
| Payments from a headless WordPress front end | Custom store, Mode B |
| Your own payment screen, but keep WooCommerce orders | Custom store, Mode B, plus your own order sync |
The last row is real work and worth naming honestly: you'd be reimplementing the order bookkeeping this plugin already does. Take it on only if the redirect is genuinely unacceptable for your checkout.
Final operational checklist
- Keep the api_key server-side. It belongs in WordPress options, never in a theme or JS file.
- Serve your site over HTTPS. Callbacks to plain HTTP are rejected in production.
- Fulfil on the order status, not on the shopper reaching your thank-you page.
- Leave the amount guard alone. It's what stops an underpaid order completing.
- Update the store's Callback URL whenever the site's domain changes.
Full HTTP contract, in six languages: the custom store guide.
Ready to start?