> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mycryptoserver.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WordPress

> Every way to accept crypto payments on a WordPress site — no-code payment links, the WooCommerce gateway plugin, or a custom integration against the REST API

There are three ways to take crypto payments on a WordPress site with MyCryptoServer, in increasing order of effort:

| Approach                                                            | Best for                                                                  | Code required           |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------- |
| [Payment links](#option-1-payment-links-no-code)                    | Fixed-price products, donations, invoices, "buy now" buttons on any page  | None                    |
| [WooCommerce plugin](#option-2-the-woocommerce-plugin)              | Full stores — cart totals, order statuses, thank-you page                 | None (install a plugin) |
| [Custom integration](#option-3-custom-integration-via-the-rest-api) | Membership plugins, custom forms, non-WooCommerce carts, anything bespoke | PHP                     |

All three work against both the hosted service and a [self-hosted instance](/self-hosting/overview) — everything goes through the public `/api/v1/*` API and hosted checkout pages.

## Option 1: Payment links (no code)

A [payment link](/api/reference/payment-links) is a reusable URL (`https://your-instance.com/l/{slug}`) with a fixed price. Every visitor who opens it gets their own checkout session and unique deposit address. No plugin needed — you just put the URL on your site.

<Steps>
  <Step title="Create the link">
    In the dashboard, go to **Payment Links → New link** and set the title, price, and currency. Optionally set:

    * **Success URL** — a page on your WordPress site to send the customer to after payment, e.g. `https://your-site.com/thank-you/`.
    * **Cancel URL** — where to send them if they back out.

    Copy the checkout URL (`/l/{slug}`).
  </Step>

  <Step title="Put it on your site">
    In the block editor, add a **Buttons** block, label it (e.g. "Pay with crypto"), and set the button's link to the checkout URL. Any other mechanism works too — a nav menu item, a classic-editor hyperlink, or a raw HTML block:

    ```html theme={null}
    <a class="wp-block-button__link" href="https://your-instance.com/l/pro-plan">
      Pay with crypto — $49.99
    </a>
    ```

    If your page already collects the customer's email (a form, a logged-in user), append it so the checkout skips the email step:

    ```php theme={null}
    $url = 'https://your-instance.com/l/pro-plan?prefill_email=' . rawurlencode( $user->user_email );
    ```
  </Step>

  <Step title="Know when you've been paid">
    Payments appear in the dashboard under **Sessions**, and you get a notification for each confirmed payment. For automated fulfillment (grant access, send a file), add a webhook — that's the [custom integration](#option-3-custom-integration-via-the-rest-api) below, which composes with payment links: the webhook fires for link payments too.
  </Step>
</Steps>

<Note>
  The success-URL redirect is a convenience, not proof of payment — a customer can open that page directly. If anything of value is delivered automatically, gate it on a webhook or on the session status from the API, never on the redirect alone.
</Note>

## Option 2: The WooCommerce plugin

If your store runs WooCommerce, use the **MyCryptoServer for WooCommerce** gateway plugin. It adds "Pay with crypto" at checkout, redirects the customer to the hosted payment page, and automatically moves the order **On hold → Processing** when the on-chain payment confirms, via a signed webhook back to your store.

Download it from **Dashboard → Integrations → WordPress**: enter your store URL, pick live or test mode, and you get a zip **pre-configured for your store** — the API key and webhook endpoint are provisioned automatically and baked into the download, so setup is upload → activate → enable the gateway. No credentials to copy-paste.

Highlights:

* WordPress 6.0+, WooCommerce 7.0+, PHP 7.4+; supports HPOS and the block-based Checkout.
* Order statuses map to session events (`session.paid` → `payment_complete()`, `session.expired` → Failed, `session.underpaid` → kept On hold for review).
* Webhook processing is idempotent, every delivery is logged, and there's a **Test send webhook** button to verify the round-trip before going live.

The full walkthrough — installation, API key scopes, webhook setup, status mapping, troubleshooting — is on the dedicated [WooCommerce page](/integrations/woocommerce).

## Option 3: Custom integration via the REST API

For anything the plugin doesn't cover — membership/LMS plugins, custom order forms, a non-WooCommerce cart — integrate directly: create a checkout session server-side, redirect the customer, and fulfill on a signed webhook. It's roughly 60 lines of PHP.

### Prerequisites

1. An API key (**API Keys → New API Key**) with the `sessions:write` scope. See [Authentication](/api/authentication).
2. A webhook endpoint (**Webhooks → Add endpoint**) pointing at the REST route you'll register below, subscribed to the `session.*` events. Copy the `whsec_...` signing secret — it's shown only once.

Keep the credentials out of the database and out of client-side code. `wp-config.php` is the conventional place:

```php theme={null}
define( 'MCS_API_BASE', 'https://your-instance.com' );
define( 'MCS_API_KEY', 'ck_live_...' );          // needs sessions:write
define( 'MCS_WEBHOOK_SECRET', 'whsec_...' );
```

### Create a session and redirect

Call `POST /api/v1/checkout_sessions` when the customer is ready to pay, store the returned session `id` against your own record, and redirect to `checkoutUrl`:

```php theme={null}
/**
 * Create a MyCryptoServer checkout session. Returns the decoded session
 * array (id, checkoutUrl, ...) or null on failure.
 */
function mysite_mcs_create_session( $fiat_total, $reference ) {
	$response = wp_remote_post( MCS_API_BASE . '/api/v1/checkout_sessions', array(
		'timeout' => 20,
		'headers' => array(
			'Authorization'   => 'Bearer ' . MCS_API_KEY,
			'Content-Type'    => 'application/json',
			// Safe to retry on timeouts without creating a duplicate session.
			'Idempotency-Key' => 'mysite-' . $reference,
		),
		'body'    => wp_json_encode( array(
			'amount'     => array(
				// The API accepts at most 2 decimal places.
				'value'    => number_format( (float) $fiat_total, 2, '.', '' ),
				'currency' => 'USD',
			),
			'metadata'   => array( 'reference' => (string) $reference ),
			'successUrl' => home_url( '/thank-you/' ),
			'cancelUrl'  => home_url( '/payment-cancelled/' ),
		) ),
	) );

	if ( is_wp_error( $response ) || 201 !== wp_remote_retrieve_response_code( $response ) ) {
		return null;
	}

	return json_decode( wp_remote_retrieve_body( $response ), true );
}

// Usage:
$session = mysite_mcs_create_session( 49.99, $order_reference );
if ( $session ) {
	// You will need this to match the webhook later.
	update_post_meta( $order_post_id, '_mcs_session_id', $session['id'] );
	wp_redirect( $session['checkoutUrl'] );
	exit;
}
```

If you sell fixed-price items you've already defined as payment links, pass `{ "linkId": "...", "metadata": {...} }` instead of `amount` — the price then comes from the link. See [Checkout Sessions](/api/reference/checkout-sessions) for both modes.

<Warning>
  Never call the API from the browser. A `ck_` key in front-end JavaScript is public — anyone can read it from the page source. Session creation belongs in PHP (or any server-side code), as above.
</Warning>

### Receive webhooks

Register a REST route and verify the signature on every delivery. Two details matter:

* **Correlate by session id.** Event payloads identify the session as `data.sessionId` and do **not** echo back your `metadata` — match deliveries to your records using the session id you stored at creation time.
* **Be idempotent.** Failed or slow responses are retried (up to 6 attempts over \~31 hours), so the same event can arrive more than once. Fulfilling twice must be harmless.

```php theme={null}
add_action( 'rest_api_init', function () {
	register_rest_route( 'mysite/v1', '/mcs-webhook', array(
		'methods'             => 'POST',
		// Authentication is the signature check inside the callback.
		'permission_callback' => '__return_true',
		'callback'            => 'mysite_mcs_handle_webhook',
	) );
} );

function mysite_mcs_handle_webhook( WP_REST_Request $request ) {
	$raw = $request->get_body();
	$sig = (string) $request->get_header( 'x-webhook-signature' ); // "t=<unix>,v1=<hex>"

	if ( ! preg_match( '/t=(\d+),v1=([0-9a-f]+)/', $sig, $m ) ) {
		return new WP_REST_Response( array( 'error' => 'bad signature' ), 401 );
	}
	list( , $timestamp, $signature ) = $m;

	// Reject stale deliveries (possible replay).
	if ( abs( time() - (int) $timestamp ) > 300 ) {
		return new WP_REST_Response( array( 'error' => 'stale timestamp' ), 401 );
	}

	// HMAC-SHA256 over "<timestamp>.<raw body>" with the whsec_ secret.
	$expected = hash_hmac( 'sha256', $timestamp . '.' . $raw, MCS_WEBHOOK_SECRET );
	if ( ! hash_equals( $expected, $signature ) ) {
		return new WP_REST_Response( array( 'error' => 'bad signature' ), 401 );
	}

	$event      = json_decode( $raw, true );
	$session_id = isset( $event['data']['sessionId'] ) ? (string) $event['data']['sessionId'] : '';

	switch ( $event['type'] ?? '' ) {
		case 'session.paid':
		case 'session.paid_late':
			// Look up the record you stored against $session_id and fulfill it.
			// Must be idempotent — deliveries are retried.
			mysite_fulfill_by_session( $session_id, $event['data']['txHash'] ?? '' );
			break;

		case 'session.underpaid':
			// Partial payment — flag for manual review rather than fulfilling.
			break;

		case 'session.expired':
		case 'session.failed':
			// Mark the pending record as abandoned.
			break;
	}

	return new WP_REST_Response( array( 'received' => true ), 200 );
}
```

Your endpoint URL is then:

```
https://your-site.com/wp-json/mysite/v1/mcs-webhook
```

Register that URL in **Webhooks → Add endpoint**. Respond `2xx` within 30 seconds — do heavy work (emails, license generation) after responding, or queue it. You can inspect and replay any delivery from **Webhooks → Deliveries**, and fire a synthetic `session.paid` at your endpoint with the [test endpoint](/api/webhooks#testing) before taking real payments. Full payload and signature details are in [Webhooks](/api/webhooks).

## Local development

The repo ships a compose file that runs WordPress with the plugin source mounted live, plus a local Anvil chain — useful for hacking on the WooCommerce plugin or testing a custom integration end to end:

```bash theme={null}
docker compose -f docker-compose.wp.yml up
```

| Service                 | Where                                                                                                     |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| WordPress 6.6 (PHP 8.2) | `http://localhost:8080` — the plugin is mounted at `wp-content/plugins/crypto-checkout`, edits apply live |
| MariaDB                 | internal (`wp-db:3306`)                                                                                   |
| Anvil (local EVM chain) | `http://localhost:8545`, chain id `31337`                                                                 |

Pair it with the main `docker-compose.yml` (web + watcher) for a fully scriptable payment loop without touching a testnet. Use a `ck_test_...` key throughout; test-mode sessions and events are flagged `livemode: false`.

<Note>
  Webhooks need a URL the MyCryptoServer instance can reach. When both stacks run in Docker on the same machine, `http://host.docker.internal:8080/wp-json/...` works; for a hosted instance pointing at a local WordPress, tunnel it (e.g. `cloudflared`, `ngrok`).
</Note>

## Security checklist

* **API key stays server-side** — in `wp-config.php` or the gateway settings, never in JavaScript, page content, or a public repo.
* **Verify every webhook signature** before acting on the payload; reject timestamps older than \~5 minutes. Both the WooCommerce plugin and the sample above do this.
* **Fulfill on webhooks, not redirects** — the success URL can be opened by anyone.
* **Serve everything over HTTPS** — webhook endpoints and checkout redirects included.
* **Scope keys minimally** — a store only needs `sessions:write` (plus `webhooks:write` if you want the plugin's Test-send button).

## Troubleshooting

| Symptom                                | Likely cause                                                                                                                                      |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` creating a session               | Wrong API base URL or key, or the key lacks `sessions:write`                                                                                      |
| `400 invalid_body` on `amount.value`   | More than 2 decimal places — normalize with `number_format( $total, 2, '.', '' )`                                                                 |
| Webhook route returns `404`            | Permalinks set to "Plain" — the REST API needs pretty permalinks (**Settings → Permalinks**, anything but Plain), or your host blocks `/wp-json/` |
| Deliveries show `401` in the dashboard | Signing secret mismatch — the `whsec_` in WordPress isn't the one for that endpoint                                                               |
| Deliveries time out                    | Your handler does slow work before responding — return `200` first, process after                                                                 |
| Nothing fires at all                   | Endpoint inactive, wrong URL, or the `session.*` events aren't subscribed — check **Webhooks → Deliveries**                                       |

WooCommerce-plugin-specific issues (gateway not visible, orders stuck On hold, Test send failures) are covered in the [WooCommerce guide](/integrations/woocommerce#troubleshooting).
