Seller Integration Guide
This guide explains how to integrate Meshpay as a seller to accept payments and gate access to your content, APIs, or services. You'll learn how to:
- Create charges when users request paid content
- Verify payments in real-time before showing content
- Receive webhook notifications for background processing
The Complete Payment Flow
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SELLER INTEGRATION FLOW │
└─────────────────────────────────────────────────────────────────────────────────┘
User Seller Backend Meshpay Blockchain
│ │ │ │
│ 1. Request content │ │ │
│ ─────────────────────────▶│ │ │
│ │ │ │
│ 2. 402 Payment Required │ │ │
│ ◀─────────────────────────│ │ │
│ (includes charge terms) │ │ │
│ │ │ │
│ 3. User pays on-chain │ │ │
│ ─────────────────────────────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ 4. Facilitator │
│ │ │◀────────────────────│
│ │ │ confirms payment │
│ │ │ │
│ │ 5. Webhook notification│ │
│ │◀────────────────────────│ │
│ │ (async, for records) │ │
│ │ │ │
│ 6. Retry with tx_id │ │ │
│ ─────────────────────────▶│ │ │
│ │ │ │
│ │ 7. Verify payment │ │
│ │ ────────────────────────▶│ │
│ │ │ │
│ │ 8. verified: true │ │
│ │◀────────────────────────│ │
│ │ │ │
│ 9. Content delivered! │ │ │
│ ◀─────────────────────────│ │ │
│ │ │ │
Two Mechanisms for Sellers
Real-Time Verification
Synchronous - Call /v1/charges/verify before showing content
- Instant verification
- Anti-cheat protection
- Required for content gating
Webhook Notifications
Asynchronous - Receive billing.transaction.succeeded webhooks
- Background processing
- Update your database
- Audit trail
Use both together: Webhooks for bookkeeping, verification for content access.
Step 1: Create a Charge
When a user requests paid content, create a charge with Meshpay:
import requestsimport osresponse = requests.post("https://api.orvion.sh/v1/charges",headers={"Authorization": f"Bearer {os.environ['MESHPAY_API_KEY']}","Content-Type": "application/json"},json={"amount": 0.99,"currency": "USD","customer_ref": "user_123","resource_ref": "article:premium-guide-42"})charge = response.json()# Return charge["id"] and charge["x402_requirements"] to frontend
Key Fields for Seller Integration
| Field | Purpose |
|-------|---------|
| customer_ref | Your internal user ID - used to verify the right user paid |
| resource_ref | The resource being purchased - used to verify the right item was paid for |
| amount | The price of the content |
| currency | Currency code (e.g., "USD", "EUR") |
The response includes:
id- Transaction ID (save this!)x402_requirements- Payment instructions for the user's wallet
Step 2: Verify Payment (Real-Time)
After the user pays, verify the payment before showing content:
/v1/charges/verifyimport requestsimport osresponse = requests.post("https://api.orvion.sh/v1/charges/verify",headers={"Authorization": f"Bearer {os.environ['MESHPAY_API_KEY']}","Content-Type": "application/json"},json={"transaction_id": "3de45a76-4b5b-400f-9d40-7fee8557e76d","customer_ref": "user_123","resource_ref": "article:premium-guide-42"})result = response.json()if result.get("verified") == True:# ✅ Payment confirmed - show the content!return {"content": "Premium content here..."}else:# ❌ Payment not verified - deny accessreturn {"error": "Payment required"}
Request Parameters
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| transaction_id | string | Required | The transaction ID returned when creating the charge | 3de45a76-4b5b-400f-9d40-7fee8557e76d |
| customer_ref | string | null | Optional | Optional: Verify the customer reference matches | user_123 |
| resource_ref | string | null | Optional | Optional: Verify the resource reference matches | article:premium-guide-42 |
Response Codes
| Code | Meaning | Action |
|------|---------|--------|
| 200 with verified: true | Payment confirmed | ✅ Show content |
| 200 with verified: false | Payment pending or refs don't match | ❌ Deny access |
| 404 | Transaction not found or wrong organization | ❌ Deny access |
| 409 | Status not succeeded or refs mismatch | ❌ Deny access |
Success Response
{
"verified": true,
"status": "succeeded",
"transaction_id": "3de45a76-4b5b-400f-9d40-7fee8557e76d",
"tx_hash": "3ehzyYwXiDZW1xFJTfrY41stbmpzqWWCLo1QphjMTY6a...",
"amount": "0.99",
"currency": "USD",
"customer_ref": "user_123",
"resource_ref": "article:premium-guide-42",
"reference": null,
"confirmed_at": "2025-12-01T12:40:07.594403+00:00"
}
Error Response (409 Conflict)
{
"verified": false,
"reason": "customer_ref_mismatch",
"detail": "Transaction customer_ref does not match request"
}
Step 3: Receive Webhooks (Background)
Configure a webhook endpoint to receive payment notifications:
Webhook Events
| Event | When | Use Case |
|-------|------|----------|
| billing.transaction.succeeded | Payment confirmed on-chain | Update database, grant entitlements |
| billing.transaction.failed | Payment failed | Log failure, notify user |
Webhook Payload
{
"event": "billing.transaction.succeeded",
"data": {
"id": "3de45a76-4b5b-400f-9d40-7fee8557e76d",
"status": "succeeded",
"tx_hash": "3ehzyYwXiDZW1xFJTfrY41stbmpzqWWCLo1QphjMTY6a...",
"amount": "0.99",
"currency": "USD",
"customer_ref": "user_123",
"resource_ref": "article:premium-guide-42",
"reference": null,
"billing_flow_id": null,
"metadata": {},
"confirmed_at": "2025-12-01T12:40:07.594403+00:00"
},
"timestamp": "2025-12-01T12:40:08.000Z"
}
Webhook Headers
| Header | Description |
|--------|-------------|
| X-Meshpay-Event-Id | Unique event ID (use as idempotency key) |
| X-Meshpay-Timestamp | UNIX timestamp when event was created |
| X-Meshpay-Signature | HMAC signature for verification |
Handling Webhooks
from flask import Flask, request, jsonifyimport hmacimport hashlibimport osapp = Flask(__name__)@app.route('/webhooks/meshpay', methods=['POST'])def handle_webhook():event_id = request.headers.get('X-Meshpay-Event-Id')timestamp = request.headers.get('X-Meshpay-Timestamp')signature = request.headers.get('X-Meshpay-Signature')# 1. Verify signature (recommended)payload = request.get_data(as_text=True)expected_signature = hmac.new(os.environ['MESHPAY_WEBHOOK_SECRET'].encode(),f"{timestamp}.{payload}".encode(),hashlib.sha256).hexdigest()if signature != expected_signature:return jsonify({'error': 'Invalid signature'}), 401# 2. Check for duplicate events (idempotency)if db.webhook_events.find_one({'event_id': event_id}):return jsonify({'status': 'already_processed'}), 200# 3. Process the eventdata = request.jsonevent = data.get('event')event_data = data.get('data')if event == 'billing.transaction.succeeded':# Grant access to the resourcedb.entitlements.insert_one({'user_id': event_data['customer_ref'],'resource_id': event_data['resource_ref'],'transaction_id': event_data['id'],'amount': event_data['amount'],'currency': event_data['currency'],})# Mark event as processeddb.webhook_events.insert_one({'event_id': event_id,'processed_at': datetime.utcnow()})return jsonify({'status': 'ok'}), 200
Complete Integration Examples
Example 1: REST API Endpoint (Pay-Per-Request)
import express from 'express'const app = express()// Middleware to check paymentasync function requirePayment(req, res, next) {const transactionId = req.headers['x-transaction-id']const userId = req.user.id // From your auth middlewareconst resourceRef = `api:${req.path}`if (!transactionId) {// No payment proof - create a charge and return 402const charge = await createCharge({amount: 0.01,currency: 'USD',customer_ref: userId,resource_ref: resourceRef,})return res.status(402).json({error: 'Payment Required',charge_id: charge.id,x402_requirements: charge.x402_requirements,})}// Verify the paymentconst verification = await verifyCharge({transaction_id: transactionId,customer_ref: userId,resource_ref: resourceRef,})if (!verification.verified) {return res.status(402).json({error: 'Payment verification failed',reason: verification.reason,})}// Payment verified - continue to the endpointreq.payment = verificationnext()}// Protected endpointapp.get('/api/premium/data', requirePayment, (req, res) => {res.json({data: 'This is premium data!',paid_amount: req.payment.amount,paid_at: req.payment.confirmed_at,})})
Example 2: Web App Content Paywall
import express from 'express'const app = express()app.get('/article/:id', async (req, res) => {const articleId = req.params.idconst userId = req.user?.id || req.cookies.anonymous_idconst transactionId = req.query.tx_id// Fetch article metadataconst article = await db.articles.findUnique({where: { id: articleId }})if (!article.is_premium) {// Free article - show contentreturn res.render('article', { article, content: article.content })}// Premium article - check paymentif (transactionId) {const verification = await verifyCharge({transaction_id: transactionId,customer_ref: userId,resource_ref: `article:${articleId}`,})if (verification.verified) {// Payment verified - show full contentreturn res.render('article', {article,content: article.content,paid: true,})}}// No payment or verification failed - show paywallconst charge = await createCharge({amount: article.price,currency: 'USD',customer_ref: userId,resource_ref: `article:${articleId}`,})res.render('article', {article,content: article.preview, // Only show previewpaywall: true,charge_id: charge.id,x402_requirements: charge.x402_requirements,})})
Example 3: AI Agent API (x402 Protocol)
from fastapi import FastAPI, Request, Responsefrom fastapi.responses import JSONResponseapp = FastAPI()@app.get("/api/generate")async def generate_content(request: Request):"""x402-compatible endpoint for AI agents.Returns 402 with payment requirements if not paid."""# Check for x402 payment headerpayment_header = request.headers.get("X-Payment")if not payment_header:# Create charge and return 402charge = await create_charge(amount=0.05,currency="USD",customer_ref=request.headers.get("X-Agent-Id", "anonymous"),resource_ref="api:generate",)return JSONResponse(status_code=402,content={"error": "Payment Required","x402": charge["x402_requirements"],"charge_id": charge["id"],},headers={"X-Payment-Required": "true","X-Payment-Amount": str(charge["x402_requirements"]["amount"]),})# Parse payment header (format: "transaction_id:tx_hash")transaction_id, tx_hash = payment_header.split(":")# Verify paymentverification = await verify_charge(transaction_id=transaction_id,customer_ref=request.headers.get("X-Agent-Id"),resource_ref="api:generate",)if not verification["verified"]:return JSONResponse(status_code=402,content={"error": "Payment verification failed","reason": verification.get("reason", "unknown"),})# Payment verified - generate contentresult = await generate_ai_content(request.query_params.get("prompt"))return {"content": result,"payment": {"transaction_id": verification["transaction_id"],"amount": verification["amount"],}}
Security Best Practices
Always Verify Server-Side
Never trust client-provided payment claims. Always call /v1/charges/verify from your backend.
Use Reference Fields
Include customer_ref and resource_ref to prevent payment reuse across users or resources.
Security Checklist
- ✅ API key in backend only - Never expose your API key to the frontend
- ✅ Verify every request - Call
/v1/charges/verifybefore showing paid content - ✅ Match customer_ref - Ensure the paying user is the requesting user
- ✅ Match resource_ref - Ensure the payment is for the requested resource
- ✅ Validate webhook signatures - Verify
X-Meshpay-Signatureheader - ✅ Idempotent webhook handling - Use
X-Meshpay-Event-Idto prevent duplicates - ✅ HTTPS only - Never send API keys or payment data over HTTP
Testing Your Integration
1. Create a Test Charge
Use small amounts (e.g., $0.01) during development.
2. Pay with Wallet
Use the Meshpay demo app or your own frontend to complete the payment.
3. Verify the Payment
Call /v1/charges/verify with the transaction ID.
4. Check Webhooks
Monitor your webhook endpoint for billing.transaction.succeeded events.
Troubleshooting
"not_found" Error
- Cause: Transaction doesn't exist or belongs to a different organization
- Fix: Ensure you're using the same API key for creating and verifying charges
"customer_ref_mismatch" Error
- Cause: The
customer_refin verification doesn't match the charge - Fix: Pass the same
customer_refwhen creating and verifying
"status_not_succeeded" Error
- Cause: Payment hasn't been confirmed yet
- Fix: Wait for the facilitator to confirm the payment, or check if payment failed
Webhooks Not Received
- Cause: Webhook endpoint not configured or not accessible
- Fix: Configure webhook URL in Dashboard → Developers → Webhooks
Related Documentation
- Payment Verification - Detailed verification API reference
- Webhooks - Complete webhook documentation
- Charges API - Creating charges
- Receiver Configs - Configure payment destinations