Integration recipes

Copy/paste examples for common backends.

Recipes follow the same pattern: create an invoice with an XMR amount, send the customer to the hosted invoice page, then fulfill your order when the invoice is confirmed.

Conceptual flow

  1. Create an invoice via POST /api/core/invoices.
  2. Optional: include checkout_continue_url to show a customer-facing Continue button after confirmation.
  3. Redirect the customer to invoice_url.
  4. Wait for invoice.confirmed via webhook (recommended) or polling.

What to store

  • Your internal order id in metadata.order_id
  • The returned invoice.id

Environment variables

Keep secrets server-side. Never expose API keys or webhook secrets in browser code.

XMRCHECKOUT_API_KEY

Used to call authenticated endpoints.

XMRCHECKOUT_WEBHOOK_SECRET

Used to verify the X-Webhook-Secret header on webhook deliveries.

XMRCHECKOUT_API_BASE_URL

Public base URL, including /api/core: https://xmr.ibeyond.xyz/api/core

BTCPay compatibility (WooCommerce)

Use the official WooCommerce Greenfield plugin with the compatibility endpoints. Keys remain view-only, and invoices stay non-custodial.

Setup steps

  1. Sign in to XMR Checkout to get your API key.
  2. Store id: returned by the login response or via GET /api/v1/stores. The stores call returns a single store (one per primary address); use the id field.
  3. Install the official BTCPay WooCommerce plugin.
  4. In WooCommerce → BTCPay settings, set:
    • Server URL: https://xmr.ibeyond.xyz (the plugin appends /api/v1).
    • Store ID: the id returned from the stores call.
    • API key: your XMR Checkout API key.
    • Payment method: XMR-CHAIN (shows as XMR_CHAIN in WooCommerce).
    • Modal checkout is supported; the hosted invoice page is recommended for clarity.

Behavior notes

  • Authorization header: Authorization: token <api_key>.
  • Status mapping: pending → New, payment detected → Processing, confirmed → final.
  • Expired and Invalid statuses map directly.
  • Webhook deliveries use the BTCPay-Sig header.
# Verify compatibility endpoints
export XMRCHECKOUT_BTCPAY_URL="https://xmr.ibeyond.xyz/api/v1"
export XMRCHECKOUT_API_KEY="xmrcheckout_..."

curl -sS "$XMRCHECKOUT_BTCPAY_URL/stores" \
  -H "Authorization: token $XMRCHECKOUT_API_KEY"

curl

Create an invoice, then poll status until it is confirmed.

# 1) Create invoice
export XMRCHECKOUT_API_BASE_URL="https://xmr.ibeyond.xyz/api/core"
export XMRCHECKOUT_API_KEY="xmrcheckout_..."

curl -sS -X POST "$XMRCHECKOUT_API_BASE_URL/invoices" \
  -H "Authorization: ApiKey $XMRCHECKOUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_xmr": "0.15",
    "confirmation_target": 2,
    "metadata": { "order_id": "ORDER-1234" }
  }'
# 1b) Create invoice from fiat (non-binding conversion)
curl -sS -X POST "$XMRCHECKOUT_API_BASE_URL/invoices" \
  -H "Authorization: ApiKey $XMRCHECKOUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "100.00",
    "currency": "USD",
    "confirmation_target": 2,
    "metadata": { "order_id": "ORDER-1234" }
  }'
# 2) Poll public status (no auth)
export INVOICE_ID="uuid-from-response"

curl -sS "$XMRCHECKOUT_API_BASE_URL/public/invoice/$INVOICE_ID"

Node.js

Example shows creating an invoice with fetch and receiving webhooks with Express.

// create-invoice.mjs
const API_BASE_URL = process.env.XMRCHECKOUT_API_BASE_URL;
const API_KEY = process.env.XMRCHECKOUT_API_KEY;

export async function createInvoice({ amountXmr, orderId }) {
  const response = await fetch(`${API_BASE_URL}/invoices`, {
    method: "POST",
    headers: {
      Authorization: `ApiKey ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount_xmr: String(amountXmr),
      confirmation_target: 10,
      metadata: { order_id: orderId },
    }),
  });

  if (!response.ok) {
    const text = await response.text().catch(() => "");
    throw new Error(`xmrcheckout create invoice failed: ${response.status} ${text}`);
  }

  return await response.json(); // includes invoice_url
}
// webhook-server.mjs
import express from "express";

const WEBHOOK_SECRET = process.env.XMRCHECKOUT_WEBHOOK_SECRET;

const app = express();
app.use(express.json({ type: "application/json" }));

app.post("/xmrcheckout/webhook", (req, res) => {
  const headerSecret = req.get("x-webhook-secret") ?? "";
  if (!WEBHOOK_SECRET || headerSecret !== WEBHOOK_SECRET) {
    return res.sendStatus(401);
  }

  const event = req.body?.event;
  const invoice = req.body?.invoice;
  const orderId = invoice?.metadata?.order_id;

  if (event === "invoice.confirmed" && orderId) {
    // Mark your order paid here.
  }

  return res.sendStatus(204);
});

app.listen(3001, () => {
  console.log("Listening on http://localhost:3001/xmrcheckout/webhook");
});

PHP

Minimal create-invoice example and a webhook receiver endpoint.

<?php
// create_invoice.php

$apiBaseUrl = getenv("XMRCHECKOUT_API_BASE_URL");
$apiKey = getenv("XMRCHECKOUT_API_KEY");

$payload = json_encode([
  "amount_xmr" => "0.15",
  "confirmation_target" => 10,
  "metadata" => ["order_id" => "ORDER-1234"],
]);

$ch = curl_init($apiBaseUrl . "/invoices");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
  "Authorization: ApiKey " . $apiKey,
  "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false || $status < 200 || $status >= 300) {
  http_response_code(500);
  echo "xmrcheckout error: HTTP " . $status;
  exit;
}

$invoice = json_decode($body, true);
echo $invoice["invoice_url"];
<?php
// webhook.php

$webhookSecret = getenv("XMRCHECKOUT_WEBHOOK_SECRET");
$headerSecret = $_SERVER["HTTP_X_WEBHOOK_SECRET"] ?? "";
if (!$webhookSecret || $headerSecret !== $webhookSecret) {
  http_response_code(401);
  exit;
}

$raw = file_get_contents("php://input");
$payload = json_decode($raw, true);
$event = $payload["event"] ?? null;
$invoice = $payload["invoice"] ?? null;
$orderId = $invoice["metadata"]["order_id"] ?? null;

if ($event === "invoice.confirmed" && $orderId) {
  // Mark your order paid here.
}

http_response_code(204);

Python

Create an invoice with requests and receive webhooks with FastAPI.

# create_invoice.py
import os
import requests

api_base_url = os.environ["XMRCHECKOUT_API_BASE_URL"].rstrip("/")
api_key = os.environ["XMRCHECKOUT_API_KEY"]

response = requests.post(
    f"{api_base_url}/invoices",
    headers={"Authorization": f"ApiKey {api_key}"},
    json={
        "amount_xmr": "0.15",
        "confirmation_target": 2,
        "metadata": {"order_id": "ORDER-1234"},
    },
    timeout=10,
)
response.raise_for_status()
invoice = response.json()
print(invoice["invoice_url"])
# webhook_server.py
import os
from fastapi import FastAPI, Header, HTTPException

app = FastAPI()
webhook_secret = os.environ["XMRCHECKOUT_WEBHOOK_SECRET"]

@app.post("/xmrcheckout/webhook", status_code=204)
async def xmrcheckout_webhook(payload: dict, x_webhook_secret: str | None = Header(default=None)):
    if not x_webhook_secret or x_webhook_secret != webhook_secret:
        raise HTTPException(status_code=401, detail="Invalid webhook secret")

    event = payload.get("event")
    invoice = payload.get("invoice") or {}
    metadata = invoice.get("metadata") or {}
    order_id = metadata.get("order_id")

    if event == "invoice.confirmed" and order_id:
        # Mark your order paid here.
        pass

    return None

Go

Create an invoice with net/http and receive webhooks with a standard handler.

// create_invoice.go
package main

import (
  "bytes"
  "encoding/json"
  "fmt"
  "io"
  "net/http"
  "os"
  "strings"
  "time"
)

type invoiceResponse struct {
  ID         string `json:"id"`
  InvoiceURL string `json:"invoice_url"`
}

func main() {
  apiBaseURL := strings.TrimRight(os.Getenv("XMRCHECKOUT_API_BASE_URL"), "/")
  apiKey := os.Getenv("XMRCHECKOUT_API_KEY")

  payload := map[string]any{
    "amount_xmr":          "0.15",
    "confirmation_target": 2,
    "metadata": map[string]any{
      "order_id": "ORDER-1234",
    },
  }

  body, _ := json.Marshal(payload)
  req, _ := http.NewRequest("POST", apiBaseURL+"/invoices", bytes.NewReader(body))
  req.Header.Set("Authorization", "ApiKey "+apiKey)
  req.Header.Set("Content-Type", "application/json")

  client := &http.Client{Timeout: 10 * time.Second}
  resp, err := client.Do(req)
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()
  if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    data, _ := io.ReadAll(resp.Body)
    panic(fmt.Sprintf("xmrcheckout create invoice failed: %d %s", resp.StatusCode, string(data)))
  }

  var invoice invoiceResponse
  if err := json.NewDecoder(resp.Body).Decode(&invoice); err != nil {
    panic(err)
  }
  fmt.Println(invoice.InvoiceURL)
}
// webhook_server.go
package main

import (
  "encoding/json"
  "net/http"
  "os"
)

type webhookPayload struct {
  Event   string `json:"event"`
  Invoice struct {
    Metadata map[string]any `json:"metadata"`
  } `json:"invoice"`
}

func main() {
  webhookSecret := os.Getenv("XMRCHECKOUT_WEBHOOK_SECRET")

  http.HandleFunc("/xmrcheckout/webhook", func(w http.ResponseWriter, r *http.Request) {
    headerSecret := r.Header.Get("X-Webhook-Secret")
    if webhookSecret == "" || headerSecret != webhookSecret {
      w.WriteHeader(http.StatusUnauthorized)
      return
    }

    var payload webhookPayload
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    if payload.Event == "invoice.confirmed" {
      if orderID, ok := payload.Invoice.Metadata["order_id"].(string); ok && orderID != "" {
        // Mark your order paid here.
        _ = orderID
      }
    }

    w.WriteHeader(http.StatusNoContent)
  })

  _ = http.ListenAndServe(":3001", nil)
}