openbcp Documentation
Download PDFAccept USDT payments on BNB Smart Chain. Hosted checkout, fully managed infrastructure, transparent per-transaction fees.
How it works
Your backend calls the openbcp API to create a payment invoice. openbcp returns a payment_url — a hosted checkout page on openbcp.com. Redirect your customer there. openbcp handles the QR code, countdown timer, live confirmation, and fee disclosure. After 3 on-chain confirmations openbcp fires a signed webhook to your server.
- Your server calls
POST /api/v1/payment→ receivespayment_url - Redirect customer to
payment_url— openbcp hosts the checkout - Customer scans QR or copies address, sends USDT on BSC
- openbcp POSTs a signed webhook to your
callback_url
Quick Start
Get your first payment running in under 5 minutes.
1. Get your API key
Go to Dashboard → Integration. Copy your X-openbcp-Api-Key. Keep it secret — only use it server-side.
2. Create a payment and redirect
import requests
resp = requests.post(
"https://openbcp.com/api/v1/payment",
headers={"X-openbcp-Api-Key": "your-api-key"},
json={
"amount_usdt": "29.99",
"external_id": "order-123",
"callback_url": "https://yoursite.com/webhooks/openbcp",
}
)
data = resp.json()
# Redirect the customer — openbcp handles everything from here
return redirect(data["payment_url"])
3. Receive the webhook
import hmac, hashlib
@app.post("/webhooks/openbcp")
def webhook():
sig = request.headers["X-openbcp-Signature"]
ts = request.headers["X-openbcp-Timestamp"]
body = request.get_data()
msg = ts.encode() + b"." + body
expected = hmac.new(
YOUR_API_KEY.encode(), msg, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
return "Unauthorized", 401
data = request.json()
status = data["status"]
if status in ("PAID", "OVERPAID"):
# Full payment confirmed — safe to fulfil
fulfill_order(data["external_id"])
elif status == "PARTIAL":
# Customer underpaid — do NOT fulfil yet
# data["shortfall"] tells you how much more is needed
# data["amount_received"] tells you what arrived so far
notify_customer_underpaid(data["external_id"], data["shortfall"])
elif status == "EXPIRED":
# Invoice timed out
if float(data.get("amount_received", 0)) > 0:
# Customer sent funds but window closed — issue a refund
initiate_refund(data["external_id"], data["amount_received"])
else:
cancel_order(data["external_id"])
elif status == "CANCELLED":
cancel_order(data["external_id"])
return "", 202 # must return 202 to acknowledge delivery
Authentication
All API requests must include your API key in the X-openbcp-Api-Key header.
X-openbcp-Api-Key: your-api-key-here
Regenerate your key under Integration → API Key → Regenerate. Old keys stop working immediately.
Hosted Checkout
openbcp hosts the payment page at https://openbcp.com/pay/<invoice_id>. You never build checkout UI — just redirect.
Why hosted?
- Trust: Customers see
openbcp.comin the browser — a known payment processor, not a random merchant domain - Transparency: The platform fee is always shown. Amount can't be faked.
- Smart QR: Encodes EIP-681 URI — wallets auto-fill token, address, and amount
- No frontend work: QR code, countdown, live polling, paid/expired states — all handled
The flow
# 1. Your server creates the invoice
POST /api/v1/payment → { payment_url: "https://openbcp.com/pay/42", ... }
# 2. Redirect customer
window.location.href = payment_url
# 3. openbcp.com/pay/42 shows:
# - Amount: 30.02 USDT (29.99 product + 0.03 platform fee)
# - QR code (EIP-681, pre-fills wallet)
# - Copy address button
# - Live countdown + auto-confirm on payment
# 4. On confirmation → webhook fires to your callback_url
Create Payment
Creates an invoice. A platform fee is automatically added to the customer-facing amount. Returns a payment_url — redirect the customer there.
Request headers
X-openbcp-Api-Key: your-api-key
Content-Type: application/json
Request body
| Field | Type | Description | |
|---|---|---|---|
| amount_usdt | string | required | Your product price in USDT. Pass as a string — e.g. "29.99". The platform fee is added on top. |
| external_id | string | optional | Your internal order/reference ID. Returned in webhooks. |
| callback_url | string | optional | HTTPS URL that receives signed webhook POSTs on status change. |
| metadata | object | optional | Arbitrary JSON dict stored with the invoice. Use this for any custom merchant data (e.g. {"price_eur": 30, "customer_id": "abc"}). |
Response — 201 Created
{
"status": "success",
"payment_url": "https://openbcp.com/pay/42", // redirect customer here
"invoice_id": 42,
"amount_usdt": "30.02", // gross — what customer pays
"merchant_amount_usdt": "29.99", // your product price
"platform_fee_usdt": "0.03", // openbcp platform fee
"deposit_address": "0xAbCd...1234",
"network": "BEP20",
"token": "USDT",
"expires_at": "2026-06-09T12:00:00Z"
}
invoice_id if the customer needs to retry.
Check Payment Status
Public endpoint — no API key needed. Returns current status and transaction list.
Response
{
"invoice_id": 42,
"payment_status": "PAID",
"amount_usdt": "30.000000", // what the invoice was created for
"amount_received": "30.000000", // total confirmed on-chain
"shortfall": "0.000000", // 0 when PAID/OVERPAID; positive when PARTIAL
"overpaid_by": "0.000000", // 0 when PAID/PARTIAL; positive when OVERPAID
"expires_at": "2026-06-09T12:00:00Z",
"confirmations": 3,
"required_confirmations": 3,
"tx_count": 1,
"transactions": [
{
"txid": "0xabc...",
"amount_usdt": "30.000000",
"confirmations": 3,
"required_confirmations": 3,
"status": "CONFIRMED"
}
]
}
Payment status lifecycle
| Status | Meaning | Fulfil order? |
|---|---|---|
UNPAID | Waiting — no transaction detected on-chain yet | No |
PARTIAL | A confirmed payment exists but is below 98% of the required amount. shortfall shows what's still needed. The checkout page shows the gap and keeps the address active. The invoice expiry is auto-extended 30 min on first partial detection. | No — wait |
PAID | Full payment received (98%–105% of expected). Safe to fulfil. | ✅ Yes |
OVERPAID | More than 105% of expected amount confirmed. overpaid_by shows the excess. Fulfil the order and consider refunding the difference. | ✅ Yes |
EXPIRED | Invoice timed out. If amount_received > 0, the customer made a partial payment before the window closed — you may need to issue a refund. | No — cancel |
CANCELLED | Customer cancelled the checkout manually. Webhook fires so you can release reserved stock. | No — cancel |
There is also an authenticated endpoint that returns the full invoice including external_id and created_at:
Error Codes
All errors return JSON with "status": "error" and a "message" field.
| HTTP | Cause |
|---|---|
| 400 | Missing or invalid fields in request body |
| 401 | Missing or invalid X-openbcp-Api-Key header |
| 404 | Invoice not found |
| 503 | Blockchain node unavailable — retry after a short delay |
| 500 | Internal error — retry with exponential backoff |
Receiving Webhook Events
openbcp sends a signed POST to your callback_url on every terminal status change — not just on payment success.
Events that trigger a webhook
| status | When | paid | Action |
|---|---|---|---|
PAID | 98%–105% of expected amount confirmed on-chain | true | ✅ Fulfil the order |
OVERPAID | More than 105% of expected amount confirmed. overpaid_by shows the excess. | true | ✅ Fulfil. Consider refunding overpaid_by. |
PARTIAL | Confirmed payment exists but is below 98% of required. shortfall shows what's still needed. Invoice expiry is auto-extended 30 min. | false | ⏳ Wait. Customer may top up. |
EXPIRED | Invoice timed out. If amount_received > 0, a partial payment was made and a refund may be needed. | false | ❌ Cancel. Refund if amount_received > 0. |
CANCELLED | Customer manually cancelled the checkout. | false | ❌ Cancel order. |
Payload
{
"invoice_id": 42,
"external_id": "order-123",
"status": "PAID", // PAID | OVERPAID | PARTIAL | EXPIRED | CANCELLED
"amount_usdt": "30.000000", // the amount the invoice was created for
"amount_received": "30.000000", // total USDT confirmed on-chain
"shortfall": "0.000000", // how much more is needed (0 when PAID/OVERPAID)
"overpaid_by": "0.000000", // how much extra was sent (0 when PAID/PARTIAL)
"deposit_address": "0x...",
"txid": "0xabc123...", // null for EXPIRED / CANCELLED with no payment
"paid": true, // false for PARTIAL / EXPIRED / CANCELLED
"paid_at": "2026-06-08T14:32:11Z"
}
Retry policy
If your endpoint does not return HTTP 202, openbcp retries with exponential backoff up to the configured maximum. Use invoice_id + status as the deduplication key.
Verifying Signatures
Every webhook includes two headers:
X-openbcp-Signature— HMAC-SHA256 of"{timestamp}.{raw_body}", keyed with your API keyX-openbcp-Timestamp— Unix timestamp of when the event was sent (reject if >5 min old)
Python
import hmac, hashlib, time
def verify_openbcp_signature(raw_body: bytes, sig: str, timestamp: str, api_key: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False # reject stale webhooks
msg = timestamp.encode() + b"." + raw_body
expected = hmac.new(api_key.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Node.js
const crypto = require('crypto');
function verifySignature(rawBody, sig, timestamp, apiKey) {
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
const msg = Buffer.concat([Buffer.from(timestamp + '.'), rawBody]);
const expected = crypto
.createHmac('sha256', apiKey)
.update(msg)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
}
Payment Lifecycle
Every invoice moves through a defined set of statuses from creation to resolution. Understanding this flow is essential for building reliable integrations.
Status flow
| Status | Meaning | Webhook fired? | Fulfil order? |
|---|---|---|---|
UNPAID | Invoice created, waiting for customer to pay. No on-chain transaction detected yet. | No | No |
PARTIAL | A confirmed transaction exists but is below 98% of the required amount. Invoice expiry is automatically extended by your configured window (default 30 min). Customer is prompted to top up to the same address. | Yes — paid: false | No — wait |
PAID | 98%–105% of expected amount confirmed on-chain (covers minor rounding/dust). Safe to fulfil. | Yes — paid: true | ✅ Yes |
OVERPAID | More than 105% of expected amount confirmed. overpaid_by shows the excess. | Yes — paid: true | ✅ Yes |
EXPIRED | Invoice timed out. If amount_received > 0, a partial payment was made — a refund record is automatically created in your Refunds tab. | Yes — paid: false | No — cancel |
CANCELLED | Customer cancelled the checkout manually. | Yes — paid: false | No — cancel |
Amount tolerance
| Customer sends | % of invoice | Result |
|---|---|---|
| 0 USDT | 0% | UNPAID → waits until expiry |
| Any amount below 98% | <98% | PARTIAL → extension window opens, address stays active |
| 98% – 105% of invoice | 98–105% | PAID ✅ |
| More than 105% | >105% | OVERPAID ✅ — overpaid_by field available |
paid == true (status PAID or OVERPAID). Never fulfil on PARTIAL — payment is incomplete.
Underpayments (PARTIAL)
When a customer sends less than 98% of the required amount, the invoice enters PARTIAL status. This happens most often when customers manually type amounts or their wallet rounds down.
What happens automatically
- Invoice status →
PARTIAL - Expiry window extended by your configured partial extension (default 30 min, max original + 2h)
- Checkout page shows: "4.00 USDT received — 1.00 USDT still needed" with QR code still active
- Webhook fires to your
callback_urlwithpaid: false,shortfall, andamount_received - If customer tops up to ≥98% → status transitions to
PAID - If timer expires before top-up → status becomes
EXPIRED, refund record is created automatically
Webhook payload for PARTIAL
{
"invoice_id": 42,
"status": "PARTIAL",
"paid": false,
"amount_usdt": "5.000000", // what the invoice was created for
"amount_received": "4.000000", // what arrived on-chain
"shortfall": "1.000000", // how much more is needed
"overpaid_by": "0.000000"
}
Recommended handler
status = data["status"]
if status in ("PAID", "OVERPAID"):
fulfill_order(data["external_id"]) # ✅ safe to fulfil
elif status == "PARTIAL":
pass # ⏳ do nothing — customer has time to top up
# log: f"Invoice {data['invoice_id']}: received {data['amount_received']}, need {data['shortfall']} more"
elif status == "EXPIRED":
if float(data.get("amount_received", 0)) > 0:
initiate_refund(data["external_id"], data["amount_received"])
cancel_order(data["external_id"])
elif status == "CANCELLED":
cancel_order(data["external_id"])
return "", 202 # always return 202 to acknowledge
Configure your extension window
Each merchant can set their own partial extension duration in Dashboard → Settings → Partial Payment Extension (5–240 minutes). The extension is applied once, on first partial detection. A hard cap prevents extensions past the original expiry + 2 hours.
Overpayments & FIFO Refund Routing
When a customer sends more than 105% of the invoice amount, the invoice becomes OVERPAID. This is still a successful payment — fulfil the order normally. The merchant can optionally refund the excess to the customer via the dashboard, with refunds routed using FIFO logic.
What happens
- Checkout shows success with a note: "You sent a little extra — your order is confirmed."
- Webhook fires with
paid: true,status: "OVERPAID", andoverpaid_byshowing the excess - Fulfil the order — no action required from the customer
- An "OVERPAID Invoices" banner appears in Dashboard → Transactions with a Refund Excess button per invoice
Webhook payload for OVERPAID
{
"status": "OVERPAID",
"paid": true,
"amount_usdt": "10.000000",
"amount_received": "12.500000",
"overpaid_by": "2.500000", // customer sent 2.50 USDT extra
"shortfall": "0.000000"
}
FIFO refund routing for multi-wallet overpayments
When multiple wallets pay an invoice and the total exceeds the required amount, the merchant clicks Refund Excess to route refunds back. openbcp uses chronological FIFO settlement — the same model banks and Stripe use for ACH overpayments.
The algorithm walks confirmed transactions in chronological order. The transaction that pushes the running total past the invoice amount has its excess portion refunded. Any subsequent transactions are refunded in full to their respective senders.
FIFO example — 5 USDT invoice, three senders
# Chronological order of confirmed transactions
T=10:00 Wallet A sends 2 USDT → running total: 2 / 5 (under, fills invoice)
T=10:05 Wallet B sends 2 USDT → running total: 4 / 5 (under, fills invoice)
T=10:10 Wallet C sends 2 USDT → running total: 6 / 5 (OVER by 1 USDT)
↑ C tipped it over — refund C's 1 USDT excess
# Result of Refund Excess:
Refund record #1: 1 USDT − gas → Wallet C (the excess portion)
Wallets A and B keep their contributions (they filled the invoice fairly).
FIFO example — invoice was already full, late payment arrives
# Chronological order
T=10:00 Wallet A sends 5 USDT → running total: 5 / 5 (invoice PAID exactly)
T=10:30 Wallet B sends 2 USDT → running total: 7 / 5 (B's entire 2 USDT is excess)
# Result of Refund Excess:
Refund record #1: 2 USDT − gas → Wallet B (B's entire late payment)
Wallet A keeps their 5 USDT (they filled the invoice on time).
Initiating a refund excess
Currently merchant-initiated via the Dashboard. Go to Dashboard → Transactions, find the OVERPAID banner at the top, and click Refund Excess on any invoice. The refund flows through your configured refund mode (auto-approve or manual review) and is sent in the next midnight batch.
Where to view processed refunds
All refunds (underpaid and overpaid) appear in Dashboard → Refunds. Use the Underpaid / Overpaid filter pills to scope the view. Each refund shows its reason label: "Refunded — Underpaid" or "Refunded — Overpaid".
Refund System
When an invoice expires with a partial payment (customer underpaid and the extension window closed), openbcp automatically creates a refund record in your Dashboard → Refunds tab.
How refunds work
- Invoice expires with
amount_received > 0 - Refund record is created for each unique sender address
- Gas fees are deducted from the refund amount (configurable in Settings)
- Depending on your refund mode — refund is auto-approved or sits in Requests tab
- Every night at 00:00 UTC — all approved refunds are batched and sent on-chain
- Webhook fires with
status: "EXPIRED"andamount_receivedso your backend can record the refund
Refund modes (per merchant)
| Mode | Behaviour | Best for |
|---|---|---|
| Manual (default) | Refunds appear in Requests tab. Merchant must approve or decline each one before it is sent. | High-value products, fraud-prone categories |
| Auto | All refunds are automatically approved and sent at midnight — no manual action needed. | Digital goods, high-volume stores |
Two refund categories — Underpaid and Overpaid
| Category | How it's created | Refund label |
|---|---|---|
| Underpaid | Auto-created when a PARTIAL invoice expires. One refund record per unique sender. | Refunded — Underpaid |
| Overpaid | Merchant-initiated from Dashboard → Transactions (OVERPAID banner → Refund Excess). FIFO routing to the wallet that tipped past 100%. | Refunded — Overpaid |
The Refunds tab provides filter pills (All / Underpaid / Overpaid) on every sub-tab so you can scope your view. Both categories share the same midnight batch, approval flow, and blacklist mechanism.
Blacklist
When you decline a refund request, it moves to the Blacklist tab. The customer must contact your support directly. You can restore a blacklisted refund at any time — it re-enters the midnight queue for next-night processing. Declining is per-transaction (not per-customer).
Unsettled Payments register
Beyond individual refund records, the system maintains a per-tenant Unsettled Payments register — a bank-grade reconciliation table that tracks every invoice with received funds that didn't complete normally. Entries stay open until resolved (Resumed, Refunded, Lapsed, or Written Off). See Unsettled Ledger for details.
EXPIRED webhook when partial payment was made
{
"status": "EXPIRED",
"paid": false,
"amount_usdt": "5.000000",
"amount_received": "4.000000", // funds received — refund has been queued
"shortfall": "1.000000"
}
invoice_id + status as your deduplication key. A customer topping up will generate both a PARTIAL and a PAID webhook — only act on PAID.
Multi-Wallet Payments
A single invoice can receive payments from unlimited wallets. The deposit address is a real BSC HD wallet — anyone can send to it from any wallet, and openbcp tracks each sender separately on-chain.
How it works
- The blockchain scanner detects every Transfer event to the deposit address.
- Each transfer creates a separate
transactionrow with itsfrom_address,amount_usdt,txid, andconfirmations. - The invoice status is computed against the sum of all confirmed transactions.
- The status endpoint returns the full transactions array so you can audit who sent what.
Example
# Invoice #42: 5 USDT requested
Wallet A sends 1 USDT → Transaction #1 (from=A)
Wallet B sends 2 USDT → Transaction #2 (from=B)
Wallet C sends 1.5 USDT → Transaction #3 (from=C)
Wallet D sends 0.5 USDT → Transaction #4 (from=D)
# Total received: 5.0 USDT → status = PAID ✓
GET /api/v1/payment/42/status
{
"payment_status": "PAID",
"amount_received": "5.000000",
"transactions": [
{"txid": "0xaaa...", "amount_usdt": "1.000000", ...},
{"txid": "0xbbb...", "amount_usdt": "2.000000", ...},
{"txid": "0xccc...", "amount_usdt": "1.500000", ...},
{"txid": "0xddd...", "amount_usdt": "0.500000", ...}
]
}
Refund routing for multi-wallet payments
- Underpaid + expired: each unique sender gets their own refund record back to their wallet, proportional to what they sent.
- Overpaid: FIFO routing — the sender whose payment tipped the invoice past 100% gets their excess portion refunded. Any subsequent senders get their full payment refunded. See Overpayments & FIFO for the algorithm.
Resume Payment
When an invoice expires with a partial payment, the merchant can resume it instead of refunding — creating a continuation invoice for the shortfall amount. The continuation reuses the same deposit address so customers who already saved the old QR code or URL can still pay it directly.
Requires X-openbcp-Api-Key. Only works on invoices with status EXPIRED and amount_received > 0.
How it works
- Original invoice #42 expires with 4/5 USDT received.
- Merchant POSTs
/api/v1/payment/42/resumewith API key. - System creates continuation invoice #99 for 1 USDT (the shortfall) — reusing the same deposit address.
- Any pending refund records for invoice #42 are cancelled (marked
RESUMED). - Merchant shares the new
payment_urlwith the customer. The old URL also still works. - Customer pays 1 USDT → continuation invoice #99 becomes PAID.
- Cascade: original invoice #42 is automatically marked PAID.
- Webhook fires on the ORIGINAL invoice_id (#42) — your integration code doesn't need to know about continuation invoices.
Response — fresh resume
{
"status": "success",
"continuation_invoice_id": 99,
"original_invoice_id": 42,
"amount_usdt": "1.000000", // shortfall only
"amount_already_received": "4.000000",
"deposit_address": "0xabc...", // SAME address as original
"payment_url": "https://openbcp.com/pay/...",
"expires_at": "2026-06-30T15:00:00Z",
"cancelled_refund_count": 1
}
Idempotency
Calling resume on the same invoice twice returns the existing continuation invoice with "resumed_existing": true — safe to retry on network failures.
Restrictions
- Only EXPIRED invoices can be resumed (not UNPAID, PAID, OVERPAID, PARTIAL, or CANCELLED).
- Must have
amount_received > 0— fully expired invoices with zero payment cannot be resumed (create a new invoice instead). - API key must belong to the merchant who owns the original invoice — strict per-tenant isolation.
- No platform fee is charged on the continuation invoice (the original already paid the fee).
When to use Resume vs Refund
| Scenario | Recommended action |
|---|---|
| Customer wants to complete the payment, still has access to wallet | Use Resume — customer pays the shortfall, order completes normally |
| Customer cannot complete the payment, wants money back | Approve the auto-created Refund — customer's wallet receives net amount |
| Customer unreachable, no resolution | Decline the Refund or Write Off the UnsettledPayment entry |
cURL example
curl -X POST "https://openbcp.com/api/v1/payment/42/resume" \
-H "X-openbcp-Api-Key: your-api-key"
Python SDK example
from paygate import PayGateClient
client = PayGateClient(base_url="https://openbcp.com", api_key="your-key")
# Customer's invoice expired with partial payment — offer to resume
resumed = client.resume_payment(invoice_id=42)
# Send customer the new payment URL
send_email(
customer.email,
subject="Complete your order",
body=f"You already paid {resumed['amount_already_received']} USDT. "
f"Pay the remaining {resumed['amount_usdt']} USDT here: "
f"{resumed['payment_url']}"
)
# When customer pays the continuation, your webhook handler receives:
# { "invoice_id": 42, "status": "PAID", ... } ← original invoice_id, not continuation!
# No changes needed to your existing webhook code.
Unsettled Payments Ledger
Every invoice that received funds but didn't complete normally is recorded in the Unsettled Payments register — a bank-grade reconciliation table. Each entry stays open until explicitly resolved.
Lifecycle
| Status | Meaning |
|---|---|
UNRESOLVED | Default — funds received, invoice expired, awaiting merchant action |
RESUMED | Merchant created a continuation invoice (via Resume API) |
RESOLVED_PAID | Continuation invoice was paid → original completed |
REFUNDED | Refund processed and sent on-chain |
LAPSED | Continuation invoice also expired without payment |
WRITTEN_OFF | Merchant explicitly closed the entry without refunding (e.g. unrecoverable funds) |
Per-tenant access
The ledger is strictly per-tenant. Each merchant only sees entries for their own invoices via Dashboard → Refunds → Unsettled Ledger. The data includes original deposit address, expected/received/shortfall amounts, continuation invoice ID (if resumed), refund ID (if refunded), and resolution notes with audit trail.
Refund REST API
For multi-tenant platforms and SaaS merchants, openbcp exposes a full Refund REST API so refund management can be automated end-to-end without ever touching the dashboard. Every refund openbcp creates (auto-created underpaid refunds and merchant-initiated overpaid refunds) is available via this API.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/refunds | List refunds with filters (status, type, invoice_id, date range, pagination) |
GET | /api/v1/refunds/{id} | Single refund with full detail incl. on-chain txid |
POST | /api/v1/refunds/{id}/approve | Programmatically approve a refund — queues for tonight's batch |
POST | /api/v1/refunds/{id}/decline | Decline a refund — moves to blacklist. Accepts {"reason": "..."} |
POST | /api/v1/refunds/{id}/restore | Restore a blacklisted refund — re-queues for next batch |
All endpoints require X-openbcp-Api-Key and return only refunds belonging to the authenticated merchant.
List with filters
GET /api/v1/refunds?status=pending&type=underpaid&page=1&limit=50
# All filter params (combine freely):
status = pending | approved | declined | processing | sent | confirmed | failed | resumed
type = underpaid | overpaid | manual
invoice_id = 42
from_date = 2026-06-01 # inclusive, YYYY-MM-DD
to_date = 2026-06-30 # inclusive
page = 1 # default 1
limit = 50 # default 50, max 200
Response shape
{
"count": 42,
"page": 1,
"limit": 50,
"results": [
{
"refund_id": 123,
"invoice_id": 199,
"external_id": "order-9001",
"type": "underpaid", // underpaid | overpaid | manual
"status": "pending", // pending | approved | declined | sent | ...
"amount_usdt": "3.970000", // NET refund amount
"gross_amount_usdt": "4.000000", // before gas
"gas_deducted_usdt": "0.030000",
"customer_address": "0xabc...",
"source_txid": "0xdef...", // incoming tx that triggered this refund
"txid": null, // on-chain refund txid once sent
"created_at": "2026-06-29T09:42:00Z",
"reviewed_at": null
}
]
}
Approve, decline, restore
curl -X POST "https://openbcp.com/api/v1/refunds/123/approve" \
-H "X-openbcp-Api-Key: YOUR_API_KEY"
curl -X POST "https://openbcp.com/api/v1/refunds/123/decline" \
-H "X-openbcp-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Customer requested credit instead"}'
curl -X POST "https://openbcp.com/api/v1/refunds/123/restore" \
-H "X-openbcp-Api-Key: YOUR_API_KEY"
Multi-tenant platform pattern
For SaaS platforms running payments on behalf of many sub-merchants (tenants), the recommended flow is:
- Receive the
EXPIREDinvoice webhook withamount_received > 0 - Look up the refund record:
GET /api/v1/refunds?invoice_id=42 - Auto-approve it programmatically:
POST /api/v1/refunds/<id>/approve - Notify the affected tenant in your own UI
- When the
refund.sentwebhook fires (see below) → mark the invoice REFUNDED in your DB and surface the txid to the tenant
Per-Type Refund Mode
Merchants can configure independent automation rules for underpaid vs overpaid refunds via API. This is important because the two scenarios usually call for different policies — underpaid funds should usually be auto-refunded (no debate), while overpaid funds may warrant manual review (e.g. offer the customer credit instead of paying gas to refund).
{
"status": "success",
"underpaid_mode": "auto",
"overpaid_mode": "manual"
}
Body: any subset of underpaid_mode and overpaid_mode. Each accepts "auto" or "manual".
curl -X POST "https://openbcp.com/api/v1/settings/refunds" \
-H "X-openbcp-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"underpaid_mode": "auto", "overpaid_mode": "manual"}'
Behaviour
- auto: refund records created in this category are automatically marked APPROVED on creation and queued for the next midnight batch — no merchant action needed.
- manual: refund records sit in PENDING status until the merchant explicitly approves or declines them (via dashboard or API).
Refund Webhook Events
Beyond the existing invoice webhooks (PAID, OVERPAID, PARTIAL, EXPIRED, CANCELLED), openbcp fires refund lifecycle webhooks so platforms can fully automate their refund workflow without polling.
Events
| Event | Fires when |
|---|---|
refund.approved | A refund is approved (auto-approval in auto mode, or merchant approval via dashboard/API). |
refund.sent | The on-chain refund transaction is broadcast (at the midnight batch). Includes txid and final amounts. |
refund.declined | A refund is declined and moved to the blacklist. |
refund.failed | The on-chain send failed (e.g. fee-account empty). Will retry next midnight. |
Same signature scheme as invoice webhooks
Refund webhooks use the exact same HMAC-SHA256 signature scheme: X-openbcp-Signature and X-openbcp-Timestamp headers, with the timestamp + body signed using your API key. The same SDK verifier function (verify_webhook) works for both.
Discriminator: every refund webhook payload has an event field starting with refund.. Your handler should branch on this field. Invoice webhooks do not have an event field (they have status).
refund.approved payload
{
"event": "refund.approved",
"refund_id": 123,
"invoice_id": 199,
"external_id": "order-9001",
"type": "underpaid",
"amount_usdt": "3.970000",
"gross_amount_usdt": "4.000000",
"gas_deducted_usdt": "0.030000",
"customer_address": "0xabc...",
"approved_by": "auto", // "auto" | "api" | "dashboard"
"approved_at": "2026-06-29T14:00:00Z"
}
refund.sent payload (most important)
{
"event": "refund.sent",
"refund_id": 123,
"invoice_id": 199,
"external_id": "order-9001",
"type": "underpaid",
"amount_usdt": "3.970000",
"gross_amount_usdt": "4.000000",
"gas_deducted_usdt": "0.030000",
"customer_address": "0xabc...",
"txid": "0xdb2750...",
"sent_at": "2026-06-30T00:03:22Z"
}
refund.declined payload
{
"event": "refund.declined",
"refund_id": 123,
"invoice_id": 199,
"external_id": "order-9001",
"declined_at": "2026-06-29T11:00:00Z",
"reason": "Customer requested credit instead"
}
refund.failed payload
{
"event": "refund.failed",
"refund_id": 123,
"invoice_id": 199,
"reason": "Have not enough tokens on fee account",
"will_retry": true,
"next_retry_at": "2026-07-01T00:00:00Z"
}
Unified webhook handler example
@app.post("/webhooks/openbcp")
def webhook():
verify_webhook(request.get_data(), ...) # same as before
data = request.json()
event = data.get("event")
if event and event.startswith("refund."):
if event == "refund.sent":
# Mark invoice REFUNDED, surface txid to tenant
mark_refunded(data["invoice_id"], data["txid"])
notify_tenant(data["external_id"], data["amount_usdt"])
elif event == "refund.failed":
alert_ops(data["refund_id"], data["reason"])
elif event == "refund.approved":
log_approval(data)
elif event == "refund.declined":
close_refund(data["refund_id"])
else:
# Invoice webhook — existing handler
handle_invoice_status(data)
return "", 202
All API Endpoints
Complete reference of every endpoint your backend can call. All authenticated endpoints require the X-openbcp-Api-Key header.
Payment lifecycle endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/v1/payment | API Key | Create a new invoice. Returns payment_url + deposit_address. |
GET | /api/v1/payment/{id} | API Key | Get full invoice details + transactions (server-side, authenticated). |
GET | /api/v1/payment/{id}/status | Public | Public status endpoint for polling. CORS-enabled. Returns amount_received, shortfall, overpaid_by, transactions. |
POST | /api/v1/payment/{id}/resume | API Key | Resume an EXPIRED-with-partial invoice. Creates continuation invoice reusing same deposit address. |
Checkout / payment page
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /pay/{token} | Public | Hosted checkout page. The token is the checkout_token returned from POST /api/v1/payment. |
GET | /pay/{token}/status | Public | Same as /api/v1/payment/{id}/status but using checkout_token. Used by the checkout page's polling. |
GET | /pay/{token}/stream | Public | Server-Sent Events stream for real-time status updates. |
POST | /pay/{token}/cancel | Public | Customer-triggered cancel from the hosted checkout. Fires CANCELLED webhook. |
Standard request headers
X-openbcp-Api-Key: your-api-key // required on all authenticated endpoints
Content-Type: application/json // for all POST requests
Accept: application/json // recommended
Webhook headers (sent BY openbcp TO your callback_url)
X-openbcp-Signature: <64-char hex> // HMAC-SHA256 of timestamp+body, secret=api_key
X-openbcp-Timestamp: 1719847200 // Unix seconds, used in signature
Content-Type: application/json
Webhook Triggers Map
Reference table showing exactly which action causes which webhook event. Use this to understand the flow without reading source code.
| Action | Triggered by | Webhook fired | paid |
|---|---|---|---|
| Invoice created via API | Your POST /api/v1/payment call | None — no webhook on creation | — |
| Customer payment confirmed on-chain (98%–105%) | Blockchain scanner detects + confirms (default 3 blocks) | PAID | true |
| Customer overpays (>105%) | Blockchain scanner + invoice.update_status | OVERPAID | true |
| Customer underpays (<98%) — first confirmation | Blockchain scanner + invoice.update_status | PARTIAL | false |
| Customer tops up partial payment to 98%+ | Blockchain scanner + invoice.update_status | PAID (after the PARTIAL one) | true |
| Invoice expires with no payment | task_expire_invoices (runs every 5 min) | EXPIRED | false |
| Invoice expires with partial payment | task_expire_invoices + auto-refund creation | EXPIRED (amount_received > 0) | false |
| Customer cancels via checkout | POST /pay/{token}/cancel from browser | CANCELLED | false |
| Merchant resumes payment | Your POST /api/v1/payment/{id}/resume call | None — internal state change only | — |
| Customer pays continuation invoice → ORIGINAL completes | Cascade-to-parent logic in _queue_notification | PAID on the ORIGINAL invoice_id | true |
202 to acknowledge. Anything else triggers exponential backoff retry (up to 7 attempts). Use invoice_id + status as your deduplication key.
Webhook payload — complete reference
The same JSON shape is sent for every status. Some fields are null or "0.000000" depending on the status.
{
"invoice_id": 42, // always present — your reference
"external_id": "order-123", // what you sent in create_payment
"status": "PAID", // PAID|OVERPAID|PARTIAL|EXPIRED|CANCELLED
"amount_usdt": "5.030000", // gross invoice amount (incl. platform fee)
"amount_received": "5.000000", // total confirmed on-chain
"shortfall": "0.000000", // 0 when PAID/OVERPAID
"overpaid_by": "0.000000", // 0 when PAID/PARTIAL
"deposit_address": "0xabc123...",
"txid": "0xdef456...", // first confirmed tx; null on EXPIRED without payment
"paid": true, // true on PAID/OVERPAID, false on others
"paid_at": "2026-06-30T15:32:11Z"
}
Complete Webhook Handler Template
Copy-paste-able handlers covering every status. These templates assume HMAC verification is already in place (see Verifying Signatures).
Python (Flask) — handles every status
from flask import Flask, request, abort
from paygate import verify_webhook, WebhookVerificationError
PAYGATE_API_KEY = "your-api-key"
@app.post("/webhooks/openbcp")
def webhook():
# 1. Verify signature FIRST — before reading any payload data
try:
verify_webhook(
request.get_data(),
request.headers["X-openbcp-Signature"],
request.headers["X-openbcp-Timestamp"],
PAYGATE_API_KEY,
)
except WebhookVerificationError:
return "Invalid signature", 401
data = request.json()
invoice_id = data["invoice_id"]
external_id = data["external_id"]
status = data["status"]
# 2. Deduplicate — use invoice_id + status as key
if already_processed(invoice_id, status):
return "", 202 # already handled, ack and exit
# 3. Dispatch on status
if status in ("PAID", "OVERPAID"):
# ✅ Safe to fulfil. data["paid"] is true here.
fulfil_order(external_id)
if status == "OVERPAID":
# Optional: log the overpayment for accounting
log_overpayment(external_id, data["overpaid_by"])
elif status == "PARTIAL":
# ⏳ Customer underpaid. data["shortfall"] = how much more needed.
# Do NOT fulfil. Could email customer asking them to top up.
notify_customer_underpaid(external_id, data["shortfall"])
elif status == "EXPIRED":
# ❌ Invoice timed out. Check if funds were received.
if float(data["amount_received"]) > 0:
# Customer paid partially — refund record was auto-created.
# You can either approve in dashboard or call /resume API.
record_unsettled(external_id, data["amount_received"])
cancel_order(external_id)
elif status == "CANCELLED":
cancel_order(external_id)
mark_processed(invoice_id, status)
return "", 202 # MUST return 202 — anything else triggers retry
Node.js (Express) — handles every status
const express = require('express');
const crypto = require('crypto');
const app = express();
const API_KEY = process.env.PAYGATE_API_KEY;
// IMPORTANT: use raw body parser for HMAC verification
app.post('/webhooks/openbcp',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-openbcp-signature'];
const ts = req.headers['x-openbcp-timestamp'];
// Verify signature
const msg = Buffer.concat([Buffer.from(ts + '.'), req.body]);
const expected = crypto.createHmac('sha256', API_KEY).update(msg).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(req.body);
switch (data.status) {
case 'PAID':
case 'OVERPAID':
fulfilOrder(data.external_id);
break;
case 'PARTIAL':
notifyUnderpaid(data.external_id, data.shortfall);
break;
case 'EXPIRED':
if (parseFloat(data.amount_received) > 0) {
recordUnsettled(data.external_id, data.amount_received);
}
cancelOrder(data.external_id);
break;
case 'CANCELLED':
cancelOrder(data.external_id);
break;
}
res.status(202).send(); // MUST be 202
}
);
cURL Cookbook
Copy-paste ready cURL commands for every endpoint. Replace YOUR_API_KEY and IDs with your values.
1. Create a payment invoice
curl -X POST "https://openbcp.com/api/v1/payment" \
-H "X-openbcp-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_usdt": "29.99",
"external_id": "order-123",
"callback_url": "https://yoursite.com/webhooks/openbcp",
"success_url": "https://yoursite.com/orders/123/success",
"metadata": {"product": "premium-plan"}
}'
2. Get full invoice details (authenticated)
curl "https://openbcp.com/api/v1/payment/42" \
-H "X-openbcp-Api-Key: YOUR_API_KEY"
3. Public status check (no auth)
curl "https://openbcp.com/api/v1/payment/42/status"
4. Resume an EXPIRED-with-partial invoice
curl -X POST "https://openbcp.com/api/v1/payment/42/resume" \
-H "X-openbcp-Api-Key: YOUR_API_KEY"
5. Poll status until paid (bash loop)
#!/bin/bash
INVOICE_ID=42
while true; do
STATUS=$(curl -s "https://openbcp.com/api/v1/payment/$INVOICE_ID/status" \
| jq -r '.payment_status')
echo "Status: $STATUS"
case "$STATUS" in
PAID|OVERPAID) echo "✓ Paid!"; break ;;
EXPIRED|CANCELLED) echo "✗ Not paid"; break ;;
*) sleep 5 ;;
esac
done
6. Stream status via Server-Sent Events
curl -N "https://openbcp.com/pay/CHECKOUT_TOKEN/stream"
7. Test your webhook handler locally (replay a webhook)
# Generate timestamp + signature
TS=$(date +%s)
BODY='{"invoice_id":42,"status":"PAID","paid":true,"amount_usdt":"5.000000","amount_received":"5.000000","shortfall":"0.000000","overpaid_by":"0.000000","deposit_address":"0xabc","txid":"0xdef","external_id":"order-123","paid_at":"2026-06-30T15:00:00Z"}'
SIG=$(printf "%s.%s" "$TS" "$BODY" | openssl dgst -sha256 -hmac "YOUR_API_KEY" | awk '{print $2}')
curl -X POST "http://localhost:3000/webhooks/openbcp" \
-H "Content-Type: application/json" \
-H "X-openbcp-Signature: $SIG" \
-H "X-openbcp-Timestamp: $TS" \
-d "$BODY"
SDK Method Reference
Every method exposed by the SDKs, what it does, what it returns. All methods raise typed exceptions on errors.
Python SDK — PayGateClient
| Method | Returns | Description |
|---|---|---|
create_payment(amount_usdt, external_id, callback_url, success_url, metadata) | Invoice | Create a new invoice and return Invoice object with payment_url. |
get_payment(invoice_id) | Invoice | Full invoice with transactions (server-side, authenticated). |
get_payment_status(invoice_id) | Invoice | Public status check (no auth). Lighter response. |
poll_until_paid(invoice_id, poll_interval, timeout) | Invoice | Block until invoice is PAID/OVERPAID. Raises on expiry/timeout. |
resume_payment(invoice_id) | dict | Resume EXPIRED-with-partial invoice. Returns continuation details. |
close() | — | Close the underlying requests Session. |
Python SDK — Invoice helper methods
| Property / Method | Returns | Description |
|---|---|---|
invoice.is_paid() | bool | True when PAID or OVERPAID — safe to fulfil. |
invoice.is_partial() | bool | True when PARTIAL — payment incomplete. |
invoice.is_expired() | bool | True when EXPIRED. |
invoice.is_cancelled() | bool | True when CANCELLED. |
invoice.is_pending() | bool | True while UNPAID or PARTIAL. |
invoice.needs_refund() | bool | True when EXPIRED/CANCELLED with amount_received > 0. |
invoice.total_confirmed | Decimal | Sum of confirmed transactions (or amount_received if available). |
Python SDK — webhook verification
| Function | Returns | Description |
|---|---|---|
verify_webhook(payload, sig, ts, api_key, max_age_sec=300) | bool | Verify HMAC-SHA256 signature. Raises WebhookVerificationError on failure or replay. |
MCP SDK — AI Assistant tools
| Tool | Args | Description |
|---|---|---|
create_payment | amount_usdt, external_id, callback_url, description | Create invoice, return payment_url to share with customer. |
get_payment | invoice_id | Full invoice with all transactions and is_paid status. |
check_payment_status | invoice_id | Quick status check with plain-English summary. Includes amount_received, shortfall, overpaid_by. |
resume_payment | invoice_id | Resume EXPIRED-with-partial invoice. AI assistants should suggest this before refunding. |
Quick install
# Python SDK
pip install git+https://github.com/usdt-paygate/paygate-python.git
# Node.js SDK
npm install github:usdt-paygate/paygate-node
# Go SDK
go get github.com/usdt-paygate/paygate-go
# MCP server (for Claude Desktop)
uvx --from git+https://github.com/usdt-paygate/paygate-mcp.git paygate-mcp
Python SDK
A thin typed wrapper around the REST API for Python backends. Includes webhook verification.
Install
pip install git+https://github.com/usdt-paygate/paygate-python.git
Usage
from paygate import PayGateClient, verify_webhook
client = PayGateClient(
base_url="https://openbcp.com",
api_key="your-api-key",
)
# Create invoice — redirect customer to payment_url
invoice = client.create_payment(
"29.99",
external_id="order-123",
callback_url="https://yoursite.com/webhooks/openbcp",
)
return redirect(invoice.payment_url) # openbcp hosts the checkout
# In your webhook handler
if verify_webhook(raw_body, sig_header, "your-api-key"):
if payload["status"] in ("PAID", "OVERPAID"):
fulfill_order(payload["external_id"])
elif payload["status"] in ("EXPIRED", "CANCELLED"):
cancel_order(payload["external_id"])
Invoice fields
| Field | Type | Description |
|---|---|---|
| payment_url | str | Hosted checkout URL — redirect the customer here |
| invoice_id | int | Numeric invoice identifier |
| amount_usdt | Decimal | Gross amount customer pays (includes fee) |
| merchant_amount_usdt | Decimal | Your product price (what you receive) |
| platform_fee_usdt | Decimal | openbcp platform fee |
| payment_status | str | UNPAID / PARTIAL / PAID / OVERPAID / EXPIRED / CANCELLED |
| amount_received | Decimal | Total USDT confirmed on-chain. Populated on status/webhook responses. |
| shortfall | Decimal | How much more is needed. 0 when PAID/OVERPAID. |
| overpaid_by | Decimal | How much extra was sent. 0 when PAID/PARTIAL. |
| expires_at | datetime | Invoice expiry (UTC). Extended on first PARTIAL detection. |
| transactions | list | On-chain transaction objects (each has from_address, amount_usdt, confirmations) |
Helper methods
| Method | Returns | Description |
|---|---|---|
invoice.is_paid() | bool | True when PAID or OVERPAID — safe to fulfil order |
invoice.is_partial() | bool | True when PARTIAL — payment incomplete, do not fulfil |
invoice.is_expired() | bool | True when EXPIRED |
invoice.is_cancelled() | bool | True when CANCELLED |
invoice.is_pending() | bool | True while still waiting (UNPAID or PARTIAL) |
invoice.needs_refund() | bool | True when funds were received but invoice cannot complete (EXPIRED/CANCELLED with amount_received > 0) |
invoice.total_confirmed | Decimal | Sum of confirmed transactions. Returns amount_received if available. |
Full webhook handler example
from paygate import verify_webhook, WebhookVerificationError
import json
@app.post("/webhooks/openbcp")
def webhook():
try:
verify_webhook(
request.get_data(),
request.headers["X-openbcp-Signature"],
request.headers["X-openbcp-Timestamp"],
YOUR_API_KEY,
)
except WebhookVerificationError:
return "Unauthorized", 401
data = request.json()
status = data["status"]
if status in ("PAID", "OVERPAID"):
fulfill_order(data["external_id"]) # ✅ fulfil
elif status == "PARTIAL":
pass # ⏳ customer topping up
elif status == "EXPIRED":
if float(data.get("amount_received", 0)) > 0:
record_pending_refund(data) # refund already queued
cancel_order(data["external_id"])
elif status == "CANCELLED":
cancel_order(data["external_id"])
return "", 202
Node.js SDK
Zero-dependency TypeScript SDK for Node 18+. Uses native fetch — no extra packages required.
Install
npm install github:usdt-paygate/paygate-node
Usage
import { PayGateClient, verifyWebhook, WebhookVerificationError } from '@openbcp/paygate';
const client = new PayGateClient({
baseUrl: 'https://openbcp.com',
apiKey: 'your-api-key',
});
// Create invoice — redirect customer to payment_url
const invoice = await client.createPayment('29.99', {
external_id: 'order-123',
callback_url: 'https://yoursite.com/webhooks/openbcp',
success_url: 'https://yoursite.com/order/confirmed',
});
res.redirect(invoice.payment_url);
// In your Express webhook handler
app.post('/webhooks/openbcp', async (req, res) => {
try {
verifyWebhook(
req.body,
req.headers['x-openbcp-signature'],
req.headers['x-openbcp-timestamp'],
process.env.PAYGATE_API_KEY,
);
} catch (e) {
if (e instanceof WebhookVerificationError) return res.sendStatus(400);
}
const data = JSON.parse(req.body);
if (['PAID', 'OVERPAID'].includes(data.status)) await fulfillOrder(data.external_id);
else if (['EXPIRED', 'CANCELLED'].includes(data.status)) await cancelOrder(data.external_id);
res.sendStatus(202);
});
API reference
| Method | Description |
|---|---|
| createPayment(amount, options?) | Create invoice. Returns Invoice with payment_url. |
| getPayment(invoiceId) | Full invoice including transactions. Requires auth. |
| getPaymentStatus(invoiceId) | Public status endpoint — no auth, CORS-enabled. |
| pollUntilPaid(invoiceId, options?) | Async poll loop — resolves when paid, rejects on EXPIRED/CANCELLED. |
| verifyWebhook(payload, sig, ts, key) | Throws WebhookVerificationError on invalid signature. |
Go SDK
Standard-library-only Go client with typed structs, context support, and HMAC webhook verification.
Install
go get github.com/usdt-paygate/paygate-go
Usage
import paygate "github.com/usdt-paygate/paygate-go"
client := paygate.NewClient("https://openbcp.com", "your-api-key")
// Create invoice
successURL := "https://yoursite.com/order/confirmed"
invoice, err := client.CreatePayment(ctx, paygate.CreatePaymentRequest{
AmountUSDT: "29.99",
ExternalID: &[]string{"order-123"}[0],
CallbackURL: &[]string{"https://yoursite.com/webhooks/openbcp"}[0],
SuccessURL: &successURL,
})
// Redirect customer to *invoice.PaymentURL
// In your webhook handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
err := paygate.VerifyWebhook(
body,
r.Header.Get("X-openbcp-Signature"),
r.Header.Get("X-openbcp-Timestamp"),
os.Getenv("PAYGATE_API_KEY"),
)
if err != nil { w.WriteHeader(http.StatusBadRequest); return }
var payload map[string]interface{}
json.Unmarshal(body, &payload)
switch payload["status"] {
case "PAID", "OVERPAID": fulfillOrder(payload["external_id"])
case "EXPIRED", "CANCELLED": cancelOrder(payload["external_id"])
}
w.WriteHeader(http.StatusAccepted)
}
API reference
| Method | Description |
|---|---|
| NewClient(baseURL, apiKey) | Create client with 20s default timeout. |
| CreatePayment(ctx, req) | Create invoice. Returns *Invoice with PaymentURL. |
| GetPayment(ctx, invoiceID) | Full invoice including transactions. Requires auth. |
| GetPaymentStatus(ctx, invoiceID) | Public status endpoint — no auth. |
| PollUntilPaid(ctx, invoiceID, interval, partial) | Blocks until paid; ctx cancellation stops the loop. |
| VerifyWebhook(payload, sig, ts, key) | Returns *WebhookVerificationError on failure. |
MCP — AI Assistant Integration
Use openbcp directly from Claude Desktop or any MCP-compatible AI. Tell Claude to create a payment in plain English — it returns the payment_url you can share instantly.
Setup
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"openbcp": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/usdt-paygate/paygate-mcp.git",
"paygate-mcp"
],
"env": {
"PAYGATE_URL": "https://openbcp.com",
"PAYGATE_API_KEY": "your-api-key"
}
}
}
}
Restart Claude Desktop. Then just type: "Create a $50 USDT payment for order 123" — Claude returns the payment_url immediately.
Available tools
| Tool | Description |
|---|---|
create_payment | Create invoice, returns payment_url to share with customer |
get_payment | Full invoice with all on-chain transactions and is_paid, total_confirmed_usdt |
check_payment_status | Quick status check. Returns amount_received, shortfall, overpaid_by, and a plain-English summary field. Understands partial/overpaid/expired-with-refund scenarios. |
Example Claude conversation
You: "Create a 25 USDT invoice for order ABC-99"
Claude: payment_url → https://openbcp.com/pay/a1b2c3...
You: "Check invoice 42"
Claude: "Partial payment received — customer sent 20.00 USDT of 25.00 USDT
required. Still waiting for 5.00 USDT more. Do NOT fulfil the order yet."
You: "Check invoice 42 again"
Claude: "Payment confirmed. Received 25.00 USDT. Fulfil the order."