Ootle Payment Gateway — Setup Guide

Tari Ootle · WooCommerce Integration

Ootle Payment Gateway for WordPress
Complete Setup Guide

Everything a new user needs to go from a blank server to a working WooCommerce checkout that accepts TARI — wallet daemon install, API key minting, plugin configuration, webhook connection, and first test payment.
Setup Guide
WooCommerce
Tari Ootle L2
October 2026
4
Components
11
Sections
6
Webhook Events
60–90
Min Setup Time

This guide assumes no prior Tari or Ootle experience. It starts from downloading the wallet daemon on a blank server and ends with a completed test payment in WooCommerce. Every link is clickable. Every command is copy-paste ready.

Before you start

You need: a Linux/macOS/Windows server with a shell · PHP 7.4+ with curl enabled · WordPress 6.0+ with WooCommerce 6.0+ installed · outbound HTTPS access to the Ootle indexer and your wallet daemon · testnet tTARI (free from the faucet) or real TARI for production.

1. Overview — how the pieces fit together

The plugin is one of three moving parts. WooCommerce handles the storefront and orders. The wallet daemon holds your keys and signs transactions. The indexer reads on-chain state and streams events. Nothing works until all three are running and reachable.

Figure 1 — System Architecture: WooCommerce → Wallet Daemon → Ootle Network

JSON-RPC Bearer tw_

submit tx events

webhook: payment event → ?wc-api=ootle_webhook

WordPress WooCommerce + Plugin checkout · orders · redirects yoursite.com

tari_ootle_walletd holds keys · signs JSON-RPC :5100 –network esmeralda|mainnet

Tari Ootle L2 validator network esmeralda testnet / mainnet payments on-chain

Ootle Indexer state + event stream ootle-indexer-a.tari.com

LEGEND Frontend (WordPress) Backend (Wallet Daemon) Database (Indexer) Network (Ootle L2) Webhook (signed, HMAC-SHA256)

Checkout flow, end to end: customer clicks “Pay with Ootle” → plugin calls the wallet daemon’s JSON-RPC to create a payment → customer pays on the Ootle network → an OrderPlaced event lands in the indexer → your webhook listener POSTs to ?wc-api=ootle_webhook → the plugin verifies the signature and marks the order complete.

2. Install the Ootle wallet daemon

The wallet daemon is the only process that holds your private keys. It signs every transaction the gateway sends. Everything else is read-only. Run it somewhere your WordPress server can reach over the network, and nowhere else.

2.1 · Download the wallet daemon

The release ships a standalone tari_ootle_walletd binary per platform. Note: there is no tari_ootle_wallet_cli binary in the release — the CLI is not published as a prebuilt asset. You interact with the daemon directly over JSON-RPC (which is what the plugin does).

Platform Asset filename (v0.45.0)
Linux x86_64 tari_ootle_walletd-0.45.0-9c62606-linux-x86_64
Linux arm64 tari_ootle_walletd-0.45.0-9c62606-linux-arm64
macOS arm64 (Apple Silicon) tari_ootle_walletd-0.45.0-9c62606-macos-arm64
Windows x64 tari_ootle_walletd-0.45.0-9c62606-windows-x64.exe

Get the current filenames and hashes from the releases page — the build hash (9c62606) changes with each release.

Download the binaryGrab the tari_ootle_walletd asset for your OS from the releases page.
Verify the checksumEach asset has a matching .sha256 file. Compare before running:

Shell Verify the download

# Linux example — paste the hash from the release's .sha256 file
echo "8cfdae03cc7dc7987752b2f1ae94046fc5c15258e1e26e0587977f7230712fae  tari_ootle_walletd-0.45.0-9c62606-linux-x86_64" | sha256sum -c -
Make it executableRun chmod +x tari_ootle_walletd in the download directory.
Place it on your PATHMove it to ~/.local/bin/ or /usr/local/bin/ (any directory already on PATH) so you can call it from anywhere.

2.2 · Choose your network

Network Flag Use case Funds
esmeralda --network esmeralda Testing, development, staging (default) Free faucet
mainnet --network mainnet Live production payments Real TARI
Start on testnet

Mistakes on testnet cost nothing. Get the entire flow working on esmeralda first, then repeat the same steps on mainnet with a fresh, funded account and a production API key.

2.3 · Start the daemon

This command runs the daemon on the esmeralda testnet, bound to localhost so only this machine reaches it. The web UI is served on the same host at port 5100.

Shell Start the wallet daemon on testnet

tari_ootle_walletd --network esmeralda --listen-on 127.0.0.1:5100 --authentication none --enable-vite-dev-port 5173
Option Meaning
–network esmeralda Connect to the esmeralda testnet (the default). Use mainnet for production.
–listen-on 127.0.0.1:5100 Bind address. Use 127.0.0.1:5100 if the plugin runs on the same machine; 0.0.0.0:5100 to accept remote connections.
–authentication none Disables WebAuthn passkey login so you can drive the daemon over JSON-RPC. Test/disposable wallets only.
–enable-vite-dev-port 5173 Sets the WebAuthn origin to http://localhost:5173 and enables permissive CORS — fixes the “invalid domain” error in the web UI.
Default ports 5100 = JSON-RPC + web UI (the plugin connects here)
Security warning you must read

The command above runs without additional daemon-level authentication — appropriate only for a disposable testnet wallet. The daemon logs a loud warning saying so, and it is correct. Never expose an unauthenticated daemon holding real value. In production, keep the daemon bound to 127.0.0.1 or a private network, and front it with an HTTPS reverse proxy plus a firewall.

Open http://localhost:5100 in your browser to reach the wallet web UI — create accounts, check balances, claim testnet funds, and manage API keys from there.

3. Create an account and fund it

3.1 · Create a merchant account

There is no wallet CLI binary in the release — you drive the daemon over its JSON-RPC endpoint. First bootstrap a bearer token (with --authentication none, this needs no passkey), then create the account.

Shell Bootstrap an Admin token and create the merchant account

# 1. Bootstrap an Admin bearer token (auth=none: no passkey, no WebAuthn)
TOKEN=$(curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"auth.request","params":{"permissions":["Admin"],"credentials":{"None":null}},"id":1}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['token'])")

# 2. Create the merchant account (returns the account + its Ootle address)
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"accounts.create","params":{"name":"merchant"},"id":2}' \
  | python3 -m json.tool

Note the owner_public_key and the otl_esm_… address from the response — you paste the public key into the plugin’s Merchant Public Key field later.

Why {“None”:null}?

Under --authentication none, auth.request expects a tagged enum credential. {"None":null} is the variant that satisfies it — this is the documented bootstrap that replaces the WebAuthn passkey flow, and it is what makes the “invalid domain” error irrelevant for programmatic access.

3.2 · Claim testnet funds (testnet only)

Two ways, both free on esmeralda:

Method How
Web UI Click “Claim Testnet Funds” at http://localhost:5100
JSON-RPC Call accounts.create_free_test_coins against the daemon (below)

The account argument is a tagged enum, not a bare name — pass the component address as {"ComponentAddress":"…"}. A claim also needs a max_fee.

Shell Claim testnet funds

ADDR="component_4293145b…"  # your account's component_address
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"accounts.create_free_test_coins\",\"params\":{\"account\":{\"ComponentAddress\":\"$ADDR\"},\"max_fee\":1000},\"id\":3}"
Faucet can be empty — check first

The shared esmeralda faucet runs dry. Before claiming, check it: accounts.get_faucet_balance returns balance and claim_amount. If balance < claim_amount the claim fails with “the testnet faucet does not hold enough funds.” That is a testnet top-up issue on Tari’s side, not a client bug — ask in the Tari Discord #development channel and retry once refilled. The alternative is claiming tTARI by burning XTM on L1, which needs real funds.

3.3 · Confirm your balance

Shell Confirm the account has a non-zero balance

curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"accounts.get_balances\",\"params\":{\"account\":{\"ComponentAddress\":\"$ADDR\"}},\"id\":4}" \
  | python3 -m json.tool

You must see a non-zero balance before moving on. An empty account cannot pay transaction fees, and every Ootle transaction pays fees in TARI.

4. Mint an API key

The plugin authenticates to the wallet daemon with a long-lived, permission-scoped API key. An Admin mints it once; the plugin then presents it as a Bearer token on every JSON-RPC call. There is no interactive login, no JWT exchange, and no session to refresh.

4.1 · How the key works

Aspect Detail
Format 32 random bytes, prefixed with tw_, base64-encoded
Shown Exactly once at creation — the daemon cannot display it again
Stored Only a SHA-256 hash — a database leak does not expose the key
Sent as Authorization: Bearer tw_... on every request
Revocation Immediate — the next request after revoke fails, no cache
Copy it the moment it appears

The raw key is displayed once, in the creation dialog, and then never again. Close the dialog and it is gone. Paste it directly into your password manager — not a text file, not a ticket, not chat.

4.2 · Choose the smallest scope set

Grant only the permissions the plugin actually needs. An agent cannot self-promote: permissions come from the stored key row, not from anything the plugin sends.

Plugin role Recommended scopes
Monitoring only AccountInfo, TransactionGet
Full payment gateway AccountInfo, TransactionSend_<your-account>, TransactionGet
Never grant Admin by default

Admin lets the key mint, list, and revoke other keys. Grant it only with a deliberate reason, and only with the explicit confirmation flag. For a payment gateway, the transaction-send scope for your one merchant account is enough.

4.3 · Create the key via Web UI (optional)

The web UI works too, but only if WebAuthn can reach it — open http://localhost:5100/api-keys and log in via passkey. If you hit an “invalid domain” error (WebAuthn rejects non-localhost origins), use the JSON-RPC method in 4.4 instead — it needs no browser at all.

Navigate to API KeysGo to the /api-keys route in the wallet web UI.
Click Create API KeyGive it a friendly name, e.g. wordpress-gateway.
Enter the scopesPaste the comma-separated scope list from the table in 4.2.
Set an expiry (recommended)Prefer the shortest expiry that fits. A leaked short-lived key dies on its own without manual revocation.
Copy the raw key immediatelyIt appears once, with a copy button. Store it before dismissing the dialog.

4.4 · Create the key over JSON-RPC (no CLI, no passkey)

With --authentication none, mint the key directly against the daemon using the bootstrap token. The raw key prints to the response once — pipe it straight into your secret store.

Shell Mint an API key for the WordPress gateway

# Basic key with two scopes
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"wordpress-gateway","permissions":["AccountInfo","TransactionGet"]},"id":3}' \
  | python3 -m json.tool

# With an expiry (unix seconds; None = never)
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"wordpress-gateway","permissions":["AccountInfo","TransactionGet"],"expires_at":1893456000},"id":4}' \
  | python3 -m json.tool

# Granting Admin requires the explicit confirm_admin flag
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"full-access","permissions":["Admin"],"confirm_admin":true},"id":5}' \
  | python3 -m json.tool
Task RPC method
List keys auth.list_api_keys — metadata only, never raw keys
Revoke auth.revoke_api_key with {"id":<n>} — effective immediately

4.5 · What the daemon protects you from

  • Wrong, revoked, or unknown key — identical error message and timing for all three; an attacker cannot enumerate valid hashes.
  • Out-of-scope request — the stored scope set is enforced against every endpoint; the request fails with InsufficientPermissions.
  • Non-admin minting keys — rejected; only Admin reaches the key-management endpoints.
  • Expired key — silently filtered out; the next request fails exactly like a revoked key.

5. Install the plugin

Log into WordPress adminAny account with plugin-install privileges works.
Go to Plugins → Add New → Upload PluginChoose the ootle-payment-gateway.zip file and click Install Now.
Activate the pluginClick Activate Plugin after installation completes.
Confirm WooCommerce is activeThe plugin requires it and silently does nothing without it. The gateway appears under WooCommerce → Settings → Payments automatically — no code edits needed.
What activation does

The plugin registers itself under gateway id ootle via the woocommerce_payment_gateways filter and flushes rewrite rules so the webhook endpoint resolves. Nothing else is written to your database until you save the settings.

6. Configure the plugin

Go to WooCommerce → Settings → Payments. You’ll see the Payment providers list — find the Ootle Payment Gateway row (badged “Test mode”) and click its Manage button to open the settings form. Fill in every field below, then Save changes at the bottom of the page.

Figure 2 — The Payments providers list; click Manage on the Ootle row

WooCommerce Payments providers list showing the Ootle Payment Gateway row with a Manage button

Figure 3 — The Ootle Payment Gateway settings page (stealth mode enabled, merchant fields filled in)

Ootle Payment Gateway settings page with Stealth (Confidential) Payments enabled and merchant account fields filled in

Field What to enter Notes
Enable Ootle Gateway Check the box Required off by default
Payment Method Title Pay with Ootle Shown to customers at checkout
Description Pay securely via the Tari Ootle network Shown under the title
API Key The tw_... key from section 4 Paste exactly — no quotes, no extra spaces
API Secret Leave blank unless signing is enabled Only needed if you turn on HMAC request signing
Webhook Secret A random 32+ character string you generate See 6.3 below
API Base URL http://your-daemon-host:5100 Your wallet daemon JSON-RPC endpoint
Test Mode Check for testnet Disables SSL verification for local/self-signed daemons

6.1 · Stealth (Confidential) Payments — the six fields

Checking Stealth (Confidential) Payments turns on private settlement: every order settles to a
one-time stealth address with a blinded amount (Pedersen commitment) and an encrypted on-chain
memo
. Six fields control this. Fill them from your wallet daemon’s JSON-RPC responses.

Field What to enter Where to get it
Stealth (Confidential) Payments Check the box Required to enable confidential settlement
Merchant Account Your account component_… address From accounts.create or accounts.list — the component_address field
Merchant Public Key The account owner_public_key From accounts.create / accounts.list — used to derive stealth addresses
Resource Address The resource_… address of your token From accounts.associate_stealth_resource — the confidential resource you accept
Resource View Key The resource view_key From accounts.associate_stealth_resource — lets the merchant decrypt received amounts
Max Fee (µT) Maximum fee per transaction, in micro-Tari Your choice — e.g. 1000 (1,000,000 µT = 1 TARI). Set a ceiling so fees never surprise you

Getting the values from the daemon

With your Admin token from section 3, run these against http://127.0.0.1:5100/json_rpc:

JSON-RPC 1. List your accounts (gives Merchant Account + Merchant Public Key)
{
  "jsonrpc": "2.0",
  "method": "accounts.list",
  "params": { "offset": 0, "limit": 10 },
  "id": 1
}

Copy component_address → Merchant Account, and owner_public_key → Merchant Public Key.

JSON-RPC 2. Associate the stealth resource (gives Resource Address + View Key)
{
  "jsonrpc": "2.0",
  "method": "accounts.associate_stealth_resource",
  "params": {
    "account": { "ComponentAddress": "component_…" },
    "resource_address": "resource_…"
  },
  "id": 2
}

The response returns the resource address and view_key. Paste them into
Resource Address and Resource View Key.

JSON-RPC 3. Confirm your account is funded (fees)
{
  "jsonrpc": "2.0",
  "method": "accounts.get_balances",
  "params": { "account": { "ComponentAddress": "component_…" } },
  "id": 3
}

Ensure the account holds enough TARI to cover the Max Fee you set, plus settlement. On testnet,
claim from the faucet with accounts.create_free_test_coins.

How a stealth checkout works: when a customer pays, the daemon derives a fresh one-time stealth
address from your Merchant Public Key, blinds the amount into a Pedersen commitment, encrypts the order
reference into the memo, then submits via accounts.stealth_transfer. Only you (holding the view key)
can decrypt the amount and memo — observers cannot link orders or read prices. The merchant account args are
tagged enums: use {"ComponentAddress":"component_…"}, never a bare string.

6.2 · Finding your API base URL

Daemon location API Base URL value
Same machine as WordPress http://127.0.0.1:5100
Separate server on your LAN http://192.168.x.x:5100
Remote VPS with a domain https://ootle.yourdomain.com (reverse-proxy the daemon)
Production must use HTTPS

In production, always front the daemon with a reverse proxy (nginx or Caddy) and a valid certificate, so the API key is never sent over plain HTTP. Test Mode disables SSL verification — it must be off in production.

6.3 · Generate a webhook secret

Run this in your terminal and paste the output into the Webhook Secret field. This secret signs outgoing webhook payloads so the plugin can prove they came from a trusted source. It is never sent to the wallet daemon.

Shell Generate the webhook secret

openssl rand -hex 32

7. Connect the webhook

Without a webhook, orders stay in “pending” forever — the plugin never learns the payment succeeded. Your indexer listener (or any monitoring service) must POST to the endpoint below whenever an on-chain payment event matches an order.

Item Value
Webhook URL https://yoursite.com/?wc-api=ootle_webhook
Method POST
Content-Type application/json
Auth header X-Ootle-Signature: HMAC-SHA256 of the raw body, keyed with your webhook secret

The payload carries an event type and a session id. The plugin looks up the order by session id and updates its status. Six event types are supported:

Event Effect on the order
payment.completed Completed payment_complete() — transaction hash logged
payment.failed Failed order marked failed
payment.cancelled Cancelled order marked cancelled
payment.refunded Refund WooCommerce refund created, hash stored
dispute.created Note dispute note added to order
dispute.resolved Resolved order updated per ruling
No listener yet?

Orders remain pending, which is expected. The payment flow still works — you complete orders manually from WooCommerce → Orders until the listener is running.

8. Test a payment

8.1 · Add a test product

Products → Add NewName it “Test Item” and set a regular price of 1.00.
PublishThe product is now purchasable.

8.2 · Place a test order

Add the test product to the cartVisit the shop page and add it.
Go to checkoutConfirm “Pay with Ootle” appears as a payment option — if it is missing, re-check that Enable Ootle Gateway is checked and saved.
Select it and place the orderThe order should redirect to your Ootle payment URL.
Check the order statusOpen WooCommerce → Orders and confirm the status is pending and the notes contain the payment URL.

8.3 · Troubleshoot connection failures

Symptom Likely cause Fix
cURL error 35: TLS connect error Self-signed cert on a local daemon Enable Test Mode in plugin settings
cURL error 7: connection refused Daemon not running, or wrong host/port Start the daemon; verify API Base URL and port
API error 401 API key wrong, mistyped, or revoked Re-copy the key; check it was not revoked
API error 403: InsufficientPermissions API key missing a required scope Add the scope and mint a new key
Order stuck on pending No webhook listener running Set up the listener (§7) or complete orders manually

9. Going to production

9.1 · Pre-launch checklist

  • Switch the network — restart the daemon with --network mainnet.
  • Turn off Test Mode — uncheck it in plugin settings so SSL verification is enforced.
  • Update the API Base URL — point at your production daemon endpoint.
  • Use HTTPS — front the daemon with a reverse proxy and a valid certificate.
  • Rotate the API key — mint a production key with production scopes; revoke the testnet key.
  • Set a key expiry — long but finite, so a leaked key eventually dies.
  • Fund the account — confirm the merchant account has mainnet TARI for fees.
  • Place one live order — a small real purchase before announcing the gateway.

9.2 · Security rules that are not optional

Rule Why
Never run an unauthenticated daemon with real funds Anyone who can reach the port can drain the account
Never commit the API key or webhook secret Version control history is permanent
Use the smallest scope set possible A leaked read-only key cannot drain the account
Set key expiry Limits how long a leaked key is useful
Revoke unused keys Fewer live keys means less attack surface
Firewall the daemon port Only the WordPress server should reach port 5100

10. Reference — settings at a glance

Plugin field Testnet value Production value
Enable Ootle Gateway yes yes
Payment Method Title Pay with Ootle Pay with Ootle
Description Pay securely via the Tari Ootle network Pay securely via the Tari Ootle network
API Key tw_... (testnet key) tw_... (production key)
API Secret blank unless signing enabled blank unless signing enabled
Webhook Secret openssl rand -hex 32 output different random output
API Base URL http://127.0.0.1:5100 https://ootle.yourdomain.com
Test Mode yes skips SSL verify no enforces SSL verify

11. Where to get help

Resource URL
Ootle documentation ootle.tari.com
API key guide ootle.tari.com/guides/agent-api-keys
Wallet setup guide ootle.tari.com/guides/setup-a-wallet
Transaction overview ootle.tari.com/guides/transaction-overview
Source code & issues github.com/tari-project/tari-ootle
Releases (binaries) github.com/tari-project/tari-ootle/releases
Community forum community.tari.com
End of guide

If a step fails, start from the symptom table in section 8.3 before escalating. Most first-time failures are a wrong port, an unchecked Test Mode box, or a missing API key scope — all three are fixable in under a minute.