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:

  1. Create charges when users request paid content
  2. Verify payments in real-time before showing content
  3. 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 requests
import os
response = 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:

POST
/v1/charges/verify
import requests
import os
response = 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 access
return {"error": "Payment required"}

Request Parameters

FieldTypeRequiredDescriptionExample
transaction_idstring
Required
The transaction ID returned when creating the charge3de45a76-4b5b-400f-9d40-7fee8557e76d
customer_refstring | nullOptionalOptional: Verify the customer reference matchesuser_123
resource_refstring | nullOptionalOptional: Verify the resource reference matchesarticle: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, jsonify
import hmac
import hashlib
import os
app = 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 event
data = request.json
event = data.get('event')
event_data = data.get('data')
if event == 'billing.transaction.succeeded':
# Grant access to the resource
db.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 processed
db.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)

Node.js
import express from 'express'
const app = express()
// Middleware to check payment
async function requirePayment(req, res, next) {
const transactionId = req.headers['x-transaction-id']
const userId = req.user.id // From your auth middleware
const resourceRef = `api:${req.path}`
if (!transactionId) {
// No payment proof - create a charge and return 402
const 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 payment
const 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 endpoint
req.payment = verification
next()
}
// Protected endpoint
app.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

Node.js
import express from 'express'
const app = express()
app.get('/article/:id', async (req, res) => {
const articleId = req.params.id
const userId = req.user?.id || req.cookies.anonymous_id
const transactionId = req.query.tx_id
// Fetch article metadata
const article = await db.articles.findUnique({
where: { id: articleId }
})
if (!article.is_premium) {
// Free article - show content
return res.render('article', { article, content: article.content })
}
// Premium article - check payment
if (transactionId) {
const verification = await verifyCharge({
transaction_id: transactionId,
customer_ref: userId,
resource_ref: `article:${articleId}`,
})
if (verification.verified) {
// Payment verified - show full content
return res.render('article', {
article,
content: article.content,
paid: true,
})
}
}
// No payment or verification failed - show paywall
const charge = await createCharge({
amount: article.price,
currency: 'USD',
customer_ref: userId,
resource_ref: `article:${articleId}`,
})
res.render('article', {
article,
content: article.preview, // Only show preview
paywall: true,
charge_id: charge.id,
x402_requirements: charge.x402_requirements,
})
})

Example 3: AI Agent API (x402 Protocol)

Python
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
app = 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 header
payment_header = request.headers.get("X-Payment")
if not payment_header:
# Create charge and return 402
charge = 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 payment
verification = 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 content
result = 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/verify before 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-Signature header
  • Idempotent webhook handling - Use X-Meshpay-Event-Id to 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_ref in verification doesn't match the charge
  • Fix: Pass the same customer_ref when 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