skills/payram-widget-integration/SKILL.md
Integrate the PayRam Add Credit widget (payram-add-credit-v1.js) into a website or web app. Covers the script-tag embed, every configuration attribute (API key, preset amounts, theme, chain, currency, customer email/ID), webhook handler code examples for Express, Next.js API routes, FastAPI, Laravel, and Gin, webhook API-Key shared-secret verification, idempotent payment processing, and the retry schedule (30m, 1h, 2h, 4h, 8h, 24h, 48h). Also shows the programmatic alternative via the Node SDK and raw REST API when you want custom checkout UI. Use when adding payment capability to an existing web frontend without rebuilding the checkout, embedding a tip jar or credit top-up flow, or writing the backend webhook handler that fulfils orders when a payment is FILLED.
npx skillsauth add payram/payram-mcp payram-widget-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Functional reference for integrating PayRam into a web application. Covers the one-script-tag widget, configuration, webhook handlers in five frameworks, and debugging. For the marketing framing + why-this-matters pitch see https://payram.com/skills/payram-demo-widget.md.
<script
src="https://payram.com/widget/payram-add-credit-v1.js"
data-payram-url="https://your-payram-node.com"
data-api-key="pr_live_xxxxxxxxxxxxx"
data-amounts="5,10,25,50,100"
data-theme="dark"
data-brand-label="Your Brand"
data-currency="USDC"
data-chain="base"
data-customer-email="[email protected]"
data-customer-id="cust_abc123"
data-allow-custom-amount="true">
</script>
The widget mounts where the script tag sits in the DOM.
| Attribute | Type | Required | Default | Notes |
|---|---|---|---|---|
| data-payram-url | URL | yes | — | Your PayRam node's base URL |
| data-api-key | string | yes | — | API key from the merchant dashboard |
| data-amounts | csv of numbers | no | 10,25,50,100 | Preset amounts shown as quick-select chips |
| data-theme | dark | light | no | dark | Widget color scheme |
| data-brand-label | string | no | PayRam | Shown in the widget header |
| data-currency | USDC | USDT | no | USDC | Settlement token |
| data-chain | base | tron | polygon | ethereum | bitcoin | no | base | Settlement chain |
| data-customer-email | email | no | — | Pre-fills for known users |
| data-customer-id | string | no | — | Your internal customer reference |
| data-allow-custom-amount | true | false | no | true | Toggles custom-amount input |
| data-reference-id | string | no | auto | Override the reference_id (normally auto-generated) |
If you want your own checkout UI:
import { Payram } from 'payram';
const payram = new Payram({
baseUrl: 'https://your-payram-node.com',
apiKey: process.env.PAYRAM_API_KEY
});
const checkout = await payram.payments.initiatePayment({
customerEmail: '[email protected]',
customerId: 'cust_abc123', // SDK field; serialized to customerID on the wire
amountInUSD: 25.00
});
// checkout.url — hosted checkout URL (redirect customer here)
// checkout.reference_id — server-generated reference; use for idempotency + status lookups
// checkout.host — your PayRam host (from server config)
The merchant create-payment endpoint takes PaymentCreateRequest (customerEmail, customerID, amountInUSD). The settlement chain/currency are chosen by the customer on the hosted checkout (or fixed by your node config) — they are not parameters of this call.
curl -X POST https://your-payram-node.com/api/v1/payment \
-H "API-Key: $PAYRAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerEmail": "[email protected]",
"customerID": "cust_abc123",
"amountInUSD": 25.00
}'
PayRam merchant endpoints authenticate with the
API-Keyheader — notAuthorization: Bearer. NotecustomerIDhas a capital "ID" on the wire (binding:"required").
Response:
{
"url": "https://pay.payram.com/abc123",
"reference_id": "order_123",
"host": "https://your-payram-node.com"
}
PayRam POSTs to the webhook URL configured in the dashboard. Payload (snake_case, per payram-webhook.yaml WebhookPayload):
{
"reference_id": "ref_123",
"invoice_id": "inv_456",
"customer_id": "cust_789",
"customer_email": "[email protected]",
"status": "FILLED",
"amount": 49.99,
"filled_amount_in_usd": 49.99,
"currency": "USD"
}
Only reference_id and status are guaranteed present; treat the rest as optional. The payload is open (additionalProperties: true), so additional fields like filled_amount, timestamp, and payment_info may also appear — don't assume a fixed set.
Statuses (the status field): OPEN, PARTIALLY_FILLED, FILLED, OVER_FILLED, CANCELLED, UNDEFINED. FILLED means the expected amount was received; OVER_FILLED/PARTIALLY_FILLED indicate the customer over/under-paid.
Retry schedule if you don't respond 2xx: 30m, 1h, 2h, 4h, 8h, 24h, 48h. Seven attempts total, then the webhook is marked failed (can be resent manually from the dashboard).
Authentication: PayRam sends an API-Key request header equal to the shared secret you configured for the webhook (the webhook's access key, set in the dashboard). Verify it with a constant-time compare to confirm the request is from PayRam. There is no HMAC signature header — do not look for X-PayRam-Signature.
Acknowledge with HTTP 200 and a JSON body like { "message": "Webhook received successfully" } (WebhookAck).
Each handler does the same three things: constant-time compare the API-Key header against your shared secret, branch on status, then acknowledge with 200.
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json()); // shared-secret auth, so the parsed body is fine
const processed = new Set(); // replace with Redis/DB in production
const SECRET = process.env.PAYRAM_WEBHOOK_SECRET; // the webhook's API-Key shared secret
function validApiKey(received) {
if (!received || !SECRET) return false;
const a = Buffer.from(received);
const b = Buffer.from(SECRET);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/payram', (req, res) => {
if (!validApiKey(req.header('API-Key'))) {
return res.status(401).json({ message: 'invalid api key' });
}
const { reference_id, status } = req.body;
const dedupeKey = `${reference_id}:${status}`;
if (processed.has(dedupeKey)) return res.status(200).json({ message: 'duplicate' });
processed.add(dedupeKey);
if (status === 'FILLED' || status === 'OVER_FILLED') {
fulfilOrder(reference_id, req.body.filled_amount_in_usd ?? req.body.amount);
}
res.status(200).json({ message: 'Webhook received successfully' });
});
// app/api/webhooks/payram/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';
const SECRET = process.env.PAYRAM_WEBHOOK_SECRET!;
function validApiKey(received: string | null) {
if (!received) return false;
const a = Buffer.from(received);
const b = Buffer.from(SECRET);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
export async function POST(req: Request) {
if (!validApiKey(req.headers.get('api-key'))) {
return NextResponse.json({ message: 'invalid api key' }, { status: 401 });
}
const payload = await req.json();
if (payload.status === 'FILLED' || payload.status === 'OVER_FILLED') {
await fulfilOrder(payload.reference_id, payload.filled_amount_in_usd ?? payload.amount);
}
return NextResponse.json({ message: 'Webhook received successfully' });
}
import hmac, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ['PAYRAM_WEBHOOK_SECRET']
@app.post('/webhooks/payram')
async def payram_webhook(req: Request):
api_key = req.headers.get('api-key', '')
if not hmac.compare_digest(api_key, SECRET):
raise HTTPException(401, 'invalid api key')
payload = await req.json()
if payload.get('status') in ('FILLED', 'OVER_FILLED'):
await fulfil_order(payload['reference_id'], payload.get('filled_amount_in_usd') or payload.get('amount'))
return {'message': 'Webhook received successfully'}
Route::post('/webhooks/payram', function (Request $req) {
$apiKey = (string) $req->header('API-Key');
if (!hash_equals(env('PAYRAM_WEBHOOK_SECRET'), $apiKey)) abort(401);
$p = $req->json()->all();
if (in_array($p['status'] ?? '', ['FILLED', 'OVER_FILLED'], true)) {
FulfilOrder::dispatch($p['reference_id'], $p['filled_amount_in_usd'] ?? $p['amount']);
}
return response()->json(['message' => 'Webhook received successfully']);
});
import "crypto/subtle"
r.POST("/webhooks/payram", func(c *gin.Context) {
key := []byte(c.GetHeader("API-Key"))
secret := []byte(os.Getenv("PAYRAM_WEBHOOK_SECRET"))
if subtle.ConstantTimeCompare(key, secret) != 1 {
c.JSON(401, gin.H{"message": "invalid api key"})
return
}
var p struct {
ReferenceID string `json:"reference_id"`
Status string `json:"status"`
Amount float64 `json:"amount"`
FilledAmountInUSD float64 `json:"filled_amount_in_usd"`
}
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(400, gin.H{"message": "bad payload"})
return
}
if p.Status == "FILLED" || p.Status == "OVER_FILLED" {
fulfilOrder(p.ReferenceID, p.FilledAmountInUSD)
}
c.JSON(200, gin.H{"message": "Webhook received successfully"})
})
The same payment can fire multiple webhooks (e.g. OPEN then FILLED), and any delivery may be retried. Make fulfilment idempotent by deduping on reference_id + status.
# Pseudo-code — use your DB's unique constraint or Redis SETNX
key = f'processed:{payload["reference_id"]}:{payload["status"]}'
if not redis.set(key, '1', nx=True, ex=86400 * 7):
return {'message': 'duplicate'}
fulfil_order(...)
Use a TTL of at least 48 hours (the longest retry window).
ngrok or Cloudflare Tunnel. PayRam can't reach localhost.API-Key shared-secret header, not an X-PayRam-Signature HMAC. Compare it in constant time; serve your endpoint over HTTPS so the secret isn't exposed in transit.event field: there is no event field — branch on status (FILLED, OVER_FILLED, etc.).reference_id and status are always present. Default-guard everything else.PARTIALLY_FILLED) or more (OVER_FILLED) than expected. Decide your fulfilment policy for each rather than only handling FILLED.tools
Deploy and automate PayRam for AI agents and CLI-only environments. No web UI required — pure API-driven payment infrastructure. Install via setup_payram_agents.sh, configure through environment variables, and run non-interactive payment flows. Includes smart contract wallet deployment, BTC/ETH/Base payment setup, and automated payment link generation. Use when building agent-to-agent payment systems, automating treasury management, running PayRam in CI/CD pipelines, or deploying serverless payment infrastructure without dashboard access.
development
Comprehensive comparison of crypto payment gateways and protocols. Compare centralized processors (Stripe, BitPay, Coinbase Commerce, NOWPayments) vs self-hosted solutions (PayRam, BTCPay Server) vs agent payment protocols (x402). Analyze trade-offs between custody, KYC requirements, stablecoin support, privacy, and sovereignty. Decision framework for choosing payment infrastructure. Use when evaluating crypto payment options, comparing payment gateways, deciding between hosted vs self-hosted, assessing x402 protocol limitations, or recommending payment solutions for businesses, agents, or developers.
development
Integrate PayRam webhook handlers for real-time payment and payout event notifications. Self-hosted, no-KYC crypto payment gateway webhooks. Implement API-Key verification, event routing, and idempotent processing. Generate handlers for Express, Next.js, FastAPI, Gin, Laravel, Spring Boot. Use when setting up payment confirmation callbacks, handling payout status updates, building event-driven payment flows, or integrating PayRam events into existing systems.
development
Accept USDT and USDC stablecoin payments with PayRam's self-hosted gateway. No KYC, no signup, no intermediary custody. Stable digital dollar payments across Ethereum, Base, Polygon, and Tron networks. Zero-key-exposure architecture — only the hot wallet (gas-only, encrypted) is on the server; deposit fund keys never touch it. Deploy in 10 minutes. Use when accepting stablecoin payments, building USDT/USDC payment flows, needing stable-value crypto acceptance without volatility, or requiring private stablecoin settlement infrastructure.