openbcp Documentation

Download PDF

Accept USDT payments on BNB Smart Chain. Hosted checkout, fully managed infrastructure, transparent per-transaction fees.

BSC transaction fees are under $0.01 regardless of amount. A small platform fee is added at invoice creation — fully disclosed to the customer on the checkout page.

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.

  1. Your server calls POST /api/v1/payment → receives payment_url
  2. Redirect customer to payment_url — openbcp hosts the checkout
  3. Customer scans QR or copies address, sends USDT on BSC
  4. openbcp POSTs a signed webhook to your callback_url
The hosted checkout QR code encodes an EIP-681 URI — wallets like Trust Wallet and MetaMask Mobile auto-fill the token, recipient, and amount when scanned.

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
Never expose your API key in frontend JavaScript, mobile apps, or public repositories. It must only be used server-side.

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?

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

POST /api/v1/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

FieldTypeDescription
amount_usdtstringrequiredYour product price in USDT. Pass as a string — e.g. "29.99". The platform fee is added on top.
external_idstringoptionalYour internal order/reference ID. Returned in webhooks.
callback_urlstringoptionalHTTPS URL that receives signed webhook POSTs on status change.
metadataobjectoptionalArbitrary 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"
}
Each invoice gets a fresh BSC deposit address. Don't create duplicate invoices for the same order — reuse the existing invoice_id if the customer needs to retry.

Check Payment Status

GET /api/v1/payment/{invoice_id}/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

StatusMeaningFulfil order?
UNPAIDWaiting — no transaction detected on-chain yetNo
PARTIALA 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
PAIDFull payment received (98%–105% of expected). Safe to fulfil.✅ Yes
OVERPAIDMore than 105% of expected amount confirmed. overpaid_by shows the excess. Fulfil the order and consider refunding the difference.✅ Yes
EXPIREDInvoice 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
CANCELLEDCustomer 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:

GET /api/v1/payment/{invoice_id} requires X-openbcp-Api-Key

Error Codes

All errors return JSON with "status": "error" and a "message" field.

HTTPCause
400Missing or invalid fields in request body
401Missing or invalid X-openbcp-Api-Key header
404Invoice not found
503Blockchain node unavailable — retry after a short delay
500Internal 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

statusWhenpaidAction
PAID98%–105% of expected amount confirmed on-chaintrue✅ Fulfil the order
OVERPAIDMore than 105% of expected amount confirmed. overpaid_by shows the excess.true✅ Fulfil. Consider refunding overpaid_by.
PARTIALConfirmed 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.
EXPIREDInvoice timed out. If amount_received > 0, a partial payment was made and a refund may be needed.false❌ Cancel. Refund if amount_received > 0.
CANCELLEDCustomer 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.

Your handler must return HTTP 202 to acknowledge delivery. Any other status code is treated as a failure and retried.

Verifying Signatures

Every webhook includes two headers:

Always verify the signature before processing the payload. Compute the HMAC on the raw request bytes — not re-serialized JSON.

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

StatusMeaningWebhook fired?Fulfil order?
UNPAIDInvoice created, waiting for customer to pay. No on-chain transaction detected yet.NoNo
PARTIALA 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: falseNo — wait
PAID98%–105% of expected amount confirmed on-chain (covers minor rounding/dust). Safe to fulfil.Yes — paid: true✅ Yes
OVERPAIDMore than 105% of expected amount confirmed. overpaid_by shows the excess.Yes — paid: true✅ Yes
EXPIREDInvoice timed out. If amount_received > 0, a partial payment was made — a refund record is automatically created in your Refunds tab.Yes — paid: falseNo — cancel
CANCELLEDCustomer cancelled the checkout manually.Yes — paid: falseNo — cancel

Amount tolerance

Customer sends% of invoiceResult
0 USDT0%UNPAID → waits until expiry
Any amount below 98%<98%PARTIAL → extension window opens, address stays active
98% – 105% of invoice98–105%PAID
More than 105%>105%OVERPAID ✅ — overpaid_by field available
Golden rule: Only fulfil orders when 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

  1. Invoice status → PARTIAL
  2. Expiry window extended by your configured partial extension (default 30 min, max original + 2h)
  3. Checkout page shows: "4.00 USDT received — 1.00 USDT still needed" with QR code still active
  4. Webhook fires to your callback_url with paid: false, shortfall, and amount_received
  5. If customer tops up to ≥98% → status transitions to PAID
  6. 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

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

  1. Invoice expires with amount_received > 0
  2. Refund record is created for each unique sender address
  3. Gas fees are deducted from the refund amount (configurable in Settings)
  4. Depending on your refund mode — refund is auto-approved or sits in Requests tab
  5. Every night at 00:00 UTC — all approved refunds are batched and sent on-chain
  6. Webhook fires with status: "EXPIRED" and amount_received so your backend can record the refund

Refund modes (per merchant)

ModeBehaviourBest 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
AutoAll refunds are automatically approved and sent at midnight — no manual action needed.Digital goods, high-volume stores

Two refund categories — Underpaid and Overpaid

CategoryHow it's createdRefund label
UnderpaidAuto-created when a PARTIAL invoice expires. One refund record per unique sender.Refunded — Underpaid
OverpaidMerchant-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"
}
Use 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

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

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.

POST /api/v1/payment/{invoice_id}/resume

Requires X-openbcp-Api-Key. Only works on invoices with status EXPIRED and amount_received > 0.

How it works

  1. Original invoice #42 expires with 4/5 USDT received.
  2. Merchant POSTs /api/v1/payment/42/resume with API key.
  3. System creates continuation invoice #99 for 1 USDT (the shortfall) — reusing the same deposit address.
  4. Any pending refund records for invoice #42 are cancelled (marked RESUMED).
  5. Merchant shares the new payment_url with the customer. The old URL also still works.
  6. Customer pays 1 USDT → continuation invoice #99 becomes PAID.
  7. Cascade: original invoice #42 is automatically marked PAID.
  8. 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

When to use Resume vs Refund

ScenarioRecommended action
Customer wants to complete the payment, still has access to walletUse Resume — customer pays the shortfall, order completes normally
Customer cannot complete the payment, wants money backApprove the auto-created Refund — customer's wallet receives net amount
Customer unreachable, no resolutionDecline 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

StatusMeaning
UNRESOLVEDDefault — funds received, invoice expired, awaiting merchant action
RESUMEDMerchant created a continuation invoice (via Resume API)
RESOLVED_PAIDContinuation invoice was paid → original completed
REFUNDEDRefund processed and sent on-chain
LAPSEDContinuation invoice also expired without payment
WRITTEN_OFFMerchant 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.

The Unsettled Ledger is your compliance and reconciliation source of truth. Every USDT that arrived for an unfinished order is tracked here until you mark it resolved.

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

MethodPathPurpose
GET/api/v1/refundsList 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}/approveProgrammatically approve a refund — queues for tonight's batch
POST/api/v1/refunds/{id}/declineDecline a refund — moves to blacklist. Accepts {"reason": "..."}
POST/api/v1/refunds/{id}/restoreRestore 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:

  1. Receive the EXPIRED invoice webhook with amount_received > 0
  2. Look up the refund record: GET /api/v1/refunds?invoice_id=42
  3. Auto-approve it programmatically: POST /api/v1/refunds/<id>/approve
  4. Notify the affected tenant in your own UI
  5. When the refund.sent webhook 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).

GET /api/v1/settings/refunds
{
  "status": "success",
  "underpaid_mode": "auto",
  "overpaid_mode":  "manual"
}
POST /api/v1/settings/refunds

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

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

EventFires when
refund.approvedA refund is approved (auto-approval in auto mode, or merchant approval via dashboard/API).
refund.sentThe on-chain refund transaction is broadcast (at the midnight batch). Includes txid and final amounts.
refund.declinedA refund is declined and moved to the blacklist.
refund.failedThe 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

MethodPathAuthPurpose
POST/api/v1/paymentAPI KeyCreate a new invoice. Returns payment_url + deposit_address.
GET/api/v1/payment/{id}API KeyGet full invoice details + transactions (server-side, authenticated).
GET/api/v1/payment/{id}/statusPublicPublic status endpoint for polling. CORS-enabled. Returns amount_received, shortfall, overpaid_by, transactions.
POST/api/v1/payment/{id}/resumeAPI KeyResume an EXPIRED-with-partial invoice. Creates continuation invoice reusing same deposit address.

Checkout / payment page

MethodPathAuthPurpose
GET/pay/{token}PublicHosted checkout page. The token is the checkout_token returned from POST /api/v1/payment.
GET/pay/{token}/statusPublicSame as /api/v1/payment/{id}/status but using checkout_token. Used by the checkout page's polling.
GET/pay/{token}/streamPublicServer-Sent Events stream for real-time status updates.
POST/pay/{token}/cancelPublicCustomer-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.

ActionTriggered byWebhook firedpaid
Invoice created via APIYour POST /api/v1/payment callNone — no webhook on creation
Customer payment confirmed on-chain (98%–105%)Blockchain scanner detects + confirms (default 3 blocks)PAIDtrue
Customer overpays (>105%)Blockchain scanner + invoice.update_statusOVERPAIDtrue
Customer underpays (<98%) — first confirmationBlockchain scanner + invoice.update_statusPARTIALfalse
Customer tops up partial payment to 98%+Blockchain scanner + invoice.update_statusPAID (after the PARTIAL one)true
Invoice expires with no paymenttask_expire_invoices (runs every 5 min)EXPIREDfalse
Invoice expires with partial paymenttask_expire_invoices + auto-refund creationEXPIRED (amount_received > 0)false
Customer cancels via checkoutPOST /pay/{token}/cancel from browserCANCELLEDfalse
Merchant resumes paymentYour POST /api/v1/payment/{id}/resume callNone — internal state change only
Customer pays continuation invoice → ORIGINAL completesCascade-to-parent logic in _queue_notificationPAID on the ORIGINAL invoice_idtrue
Webhook delivery contract: Your endpoint must return HTTP 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

MethodReturnsDescription
create_payment(amount_usdt, external_id, callback_url, success_url, metadata)InvoiceCreate a new invoice and return Invoice object with payment_url.
get_payment(invoice_id)InvoiceFull invoice with transactions (server-side, authenticated).
get_payment_status(invoice_id)InvoicePublic status check (no auth). Lighter response.
poll_until_paid(invoice_id, poll_interval, timeout)InvoiceBlock until invoice is PAID/OVERPAID. Raises on expiry/timeout.
resume_payment(invoice_id)dictResume EXPIRED-with-partial invoice. Returns continuation details.
close()Close the underlying requests Session.

Python SDK — Invoice helper methods

Property / MethodReturnsDescription
invoice.is_paid()boolTrue when PAID or OVERPAID — safe to fulfil.
invoice.is_partial()boolTrue when PARTIAL — payment incomplete.
invoice.is_expired()boolTrue when EXPIRED.
invoice.is_cancelled()boolTrue when CANCELLED.
invoice.is_pending()boolTrue while UNPAID or PARTIAL.
invoice.needs_refund()boolTrue when EXPIRED/CANCELLED with amount_received > 0.
invoice.total_confirmedDecimalSum of confirmed transactions (or amount_received if available).

Python SDK — webhook verification

FunctionReturnsDescription
verify_webhook(payload, sig, ts, api_key, max_age_sec=300)boolVerify HMAC-SHA256 signature. Raises WebhookVerificationError on failure or replay.

MCP SDK — AI Assistant tools

ToolArgsDescription
create_paymentamount_usdt, external_id, callback_url, descriptionCreate invoice, return payment_url to share with customer.
get_paymentinvoice_idFull invoice with all transactions and is_paid status.
check_payment_statusinvoice_idQuick status check with plain-English summary. Includes amount_received, shortfall, overpaid_by.
resume_paymentinvoice_idResume 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

FieldTypeDescription
payment_urlstrHosted checkout URL — redirect the customer here
invoice_idintNumeric invoice identifier
amount_usdtDecimalGross amount customer pays (includes fee)
merchant_amount_usdtDecimalYour product price (what you receive)
platform_fee_usdtDecimalopenbcp platform fee
payment_statusstrUNPAID / PARTIAL / PAID / OVERPAID / EXPIRED / CANCELLED
amount_receivedDecimalTotal USDT confirmed on-chain. Populated on status/webhook responses.
shortfallDecimalHow much more is needed. 0 when PAID/OVERPAID.
overpaid_byDecimalHow much extra was sent. 0 when PAID/PARTIAL.
expires_atdatetimeInvoice expiry (UTC). Extended on first PARTIAL detection.
transactionslistOn-chain transaction objects (each has from_address, amount_usdt, confirmations)

Helper methods

MethodReturnsDescription
invoice.is_paid()boolTrue when PAID or OVERPAID — safe to fulfil order
invoice.is_partial()boolTrue when PARTIAL — payment incomplete, do not fulfil
invoice.is_expired()boolTrue when EXPIRED
invoice.is_cancelled()boolTrue when CANCELLED
invoice.is_pending()boolTrue while still waiting (UNPAID or PARTIAL)
invoice.needs_refund()boolTrue when funds were received but invoice cannot complete (EXPIRED/CANCELLED with amount_received > 0)
invoice.total_confirmedDecimalSum 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

MethodDescription
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.

GitHub: usdt-paygate/paygate-node ↗

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

MethodDescription
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.

GitHub: usdt-paygate/paygate-go ↗

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

ToolDescription
create_paymentCreate invoice, returns payment_url to share with customer
get_paymentFull invoice with all on-chain transactions and is_paid, total_confirmed_usdt
check_payment_statusQuick 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."