Penny API
Accept crypto on your site with one API call. You create an order, we hand you a hosted payment page, the customer pays, and the money lands directly in your wallet. Penny is non-custodial: we never hold, touch, or forward your funds, and we never see a private key.
/orders with your API key → get back a pay_url → send your customer there → get a signed webhook when they've paid. That's the whole integration.
Quickstart
- Create your account at https://penny.hifriendbot.com/signup/. Approval is instant and your API key is shown once at signup (you can rotate it any time in the dashboard's Developers tab).
- Add your wallet in the dashboard's Wallets tab. Paste whatever your wallet app gives you: a Bitcoin address, an Ethereum-style 0x address, a Solana address, or an xpub. Auto-detect figures out the rest.
- Create your first order with the call below, open the
pay_urlit returns, and pay yourself a dollar to see the whole flow.
curl -X POST https://penny.hifriendbot.com/wp-json/penny/v1/orders \
-H "X-Penny-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 1.00, "external_ref": "hello-world"}'
Payment links (no code at all)
Not a developer? You never have to touch the API. In your dashboard, open Payment links, type an amount, and get a link you can text, email, or put on an invoice. The customer picks a coin and pays; you watch it confirm in the Orders tab. That's a complete crypto checkout with zero setup.
Authentication
Every merchant API call carries your key in the X-Penny-Key header. Keys start with pg_, are stored hashed on our side, and can be rotated in the dashboard (the old key stops working immediately). Keep it server-side; never put it in front-end code.
-H "X-Penny-Key: pg_your_key_here"
Create an order
POST https://penny.hifriendbot.com/wp-json/penny/v1/orders
| Field | Required | Description |
|---|---|---|
amount | yes | USD amount, e.g. 25.00 |
external_ref | no | Your order ID. Echoed back in webhooks so you can match payments to orders. |
success_url | no | Where we send the customer after payment confirms. http or https. |
cancel_url | no | Linked if the customer backs out. http or https. |
webhook_url | no | We POST signed payment events here (see Webhooks). Must be a public https URL with a hostname; loopback and private addresses are refused with 400 bad_webhook_url. |
Response:
{
"order_token": "2f2089583b5c1dc0105eee42b249f60f",
"pay_url": "https://penny.hifriendbot.com/pay/2f2089583b.../",
"status": "pending"
}
Redirect your customer to pay_url. PHP example:
$res = wp_remote_post('https://penny.hifriendbot.com/wp-json/penny/v1/orders', [
'headers' => [
'X-Penny-Key' => PENNY_KEY,
'Content-Type' => 'application/json',
],
'body' => json_encode([
'amount' => 25.00,
'external_ref' => 'order-1042',
'success_url' => 'https://yourstore.com/thanks',
'webhook_url' => 'https://yourstore.com/pg-webhook',
]),
]);
$order = json_decode(wp_remote_retrieve_body($res), true);
wp_redirect($order['pay_url']);
Check an order
GET https://penny.hifriendbot.com/wp-json/penny/v1/orders/{order_token} with your API key returns the order's current state (pending, awaiting, seen, confirmed, expired, underpaid), the coin, and the amount received. Poll it if you'd rather not use webhooks.
The hosted pay page
The pay_url page handles everything customer-facing: coin choice, a locked exchange rate, a fresh or dedicated deposit address, QR code, copy buttons, network warnings ("Send USDC on the Base network only"), live status, and auto-redirect to your success_url on confirmation.
- Customers get 90 minutes to pay, with the rate locked the whole time.
- Every order is quoted a unique amount. The last digits of the amount differ by a fraction of a cent from order to order, which is how Penny tells two customers apart when they pay the same wallet at the same time. The pay page tells the customer to send exactly the amount shown, and the QR code carries it.
- Underpayments within 2% (sender-side fees, rounding) still count as paid. Overpayments are yours; any adjustment with the customer is between you and them.
- Each incoming transaction is read individually from the blockchain and matched to one order. A transaction can never pay two orders.
- Confirmation speed depends on the chain: seconds on Solana, minutes on Base and Polygon, 10 to 60 minutes on Bitcoin. The exact confirmation counts are in the table below.
Webhooks
If you set a webhook_url, we POST JSON to it on payment.seen (detected on chain) and payment.confirmed (confirmation depth reached; safe to fulfill).
{
"event": "payment.confirmed",
"order_token": "2f20...",
"external_ref": "order-1042",
"status": "confirmed",
"currency": "usdc-base",
"amount_received": "25.0000000000",
"fiat_amount": 25.0,
"txid": "0xabc...",
"timestamp": 1751830000
}
Verify the signature. Every webhook carries X-Penny-Signature: an HMAC-SHA256 of the raw request body using your webhook secret (dashboard → Developers). Reject anything that doesn't match:
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_PENNY_SIGNATURE'] ?? '';
if (!hash_equals(hash_hmac('sha256', $raw, PENNY_WEBHOOK_SECRET), $sig)) {
http_response_code(403); exit;
}
$event = json_decode($raw, true);
if ($event['event'] === 'payment.confirmed') {
// mark your order paid, using $event['external_ref']
}
Webhooks are fire-and-forget with no retries yet, so pair them with a GET /orders/{token} reconciliation check for anything critical. Webhooks are only ever sent to public https endpoints; we validate the URL when the order is created and again at send time.
WooCommerce
Create the order with external_ref set to your WooCommerce order ID and point webhook_url at your store. On payment.confirmed, matching Woo orders are completed automatically. A one-click Woo plugin is on the roadmap; the API above works today.
Coins & confirmations
An order is reported as seen as soon as the payment appears on the network and as confirmed once it has the number of confirmations below. Fulfill on confirmed.
| Coin | Network | Confirmations to mark paid | Typical wait |
|---|---|---|---|
| Bitcoin (BTC) | Bitcoin | 1 block | 10 to 60 min |
| Ethereum (ETH) | Ethereum | 12 blocks | about 3 min |
| USDC (Ethereum) (USDC) | Ethereum | 12 blocks | about 3 min |
| USDT (Ethereum) (USDT) | Ethereum | 12 blocks | about 3 min |
| DAI (Ethereum) (DAI) | Ethereum | 12 blocks | about 3 min |
| PYUSD (PayPal USD) (PYUSD) | Ethereum | 12 blocks | about 3 min |
| USDC (Base) (USDC) | Base | 5 blocks | under 1 min |
| USDT (Base) (USDT) | Base | 5 blocks | under 1 min |
| Polygon (POL) | Polygon | 50 blocks | about 2 min |
| BNB (BNB Chain) (BNB) | BNB Chain | 15 blocks | under 1 min |
| PulseChain (PLS) | PulseChain | 20 blocks | about 4 min |
| Solana (SOL) | Solana | finalized | about 15 sec |
The customer picks the coin on the pay page; you don't choose it at order time. Every coin pays out to the wallet you configured for its chain. A payment that is still unconfirmed never marks an order paid, so a replaced or dropped transaction cannot count.
BNB Chain only: one open order per wallet address at a time. If a second customer picks BNB while another BNB payment to the same wallet is in progress, they are asked to choose a different coin or try again in a few minutes. Every other coin has no such limit.
Worth knowing: USDC, USDT, and PYUSD balances can be frozen by their issuers (Circle, Tether, and Paxos) on any chain. Native coins (BTC, ETH, SOL, POL, BNB, PLS) and DAI have no freeze mechanism, so merchants in high-scrutiny industries often prefer them.
Account security
- Penny holds no private keys and no funds. A compromised Penny account cannot move money you already received; the only thing an attacker could change is where future payments go, which is why every payout wallet change emails you immediately.
- Logins are rate-limited per account and per connection, and login, signup and password reset are protected by Cloudflare Turnstile.
- Forgot your password? Use https://penny.hifriendbot.com/forgot/. Reset links expire in one hour.
- API keys are stored hashed and shown once. Rotate a key from the Developers tab if you think it leaked; the old one stops working immediately.
- Verify
X-Penny-Signatureon every webhook and fulfill only onpayment.confirmed.
Fees & billing
No monthly cost. You pay a small percentage (0.99% founding rate; accounts created now keep that rate for life) of completed sales only, billed after the money is already in your wallet. When your balance reaches $25 or is a week old, you get a one-click settle link, and you can pay any time from the dashboard. Load prepaid credit and get 10% extra free. If a bill goes unpaid for 72 hours, new checkouts pause (in-flight payments always finish); paying instantly reactivates you.
Go-live checklist
- Wallet added for every coin family you want to accept (one 0x covers all EVM chains; add Bitcoin and Solana separately).
- API key stored server-side, never in front-end code.
- Webhook endpoint verifies
X-Penny-Signaturebefore trusting anything. - Run one real payment for a dollar and watch it confirm in the dashboard.
- Fulfill on
payment.confirmed, notpayment.seen.
Need a hand? We offer setup and consulting
Not everyone has a developer, and you shouldn't need one to get paid. We offer consulting and full setup services: we'll connect Penny to your store, wire up your wallets the right way, test a live payment with you, and hand you a working checkout.