Payment Verification
The Payment Verification endpoint allows sellers to confirm that a payment has been completed before granting access to paid content. This is the anti-cheat mechanism that prevents users from accessing content without paying.
Why Verify Payments?
Without server-side verification, a malicious user could:
- Claim they paid when they didn't
- Reuse someone else's payment proof
- Access content for a different resource than what they paid for
The /v1/charges/verify endpoint solves this by:
- Confirming the transaction exists and belongs to your organization
- Checking that the payment status is
succeeded - Optionally validating
customer_refandresource_refmatch
Endpoint
/v1/charges/verifyAuthorization: Bearer <MESHPAY_API_KEY>
Content-Type: application/json
Request Body
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| transaction_id | string | Required | The transaction ID returned when the charge was created | 3de45a76-4b5b-400f-9d40-7fee8557e76d |
| customer_ref | string | null | Optional | If provided, verifies that the transaction's customer_ref matches this value | user_123 |
| resource_ref | string | null | Optional | If provided, verifies that the transaction's resource_ref matches this value | article:premium-guide-42 |
Code Examples
Basic Verification
import 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"})result = response.json()print("Payment confirmed!" if result.get("verified") else "Payment not verified")
With Reference Validation
import 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"):print(f"Payment verified for user {result['customer_ref']}")print(f"Resource: {result['resource_ref']}")print(f"Amount: {result['amount']} {result['currency']}")else:print(f"Verification failed: {result.get('reason')}")
Response
Success Response (200 OK)
When the payment is verified:
{
"verified": true,
"status": "succeeded",
"transaction_id": "3de45a76-4b5b-400f-9d40-7fee8557e76d",
"tx_hash": "3ehzyYwXiDZW1xFJTfrY41stbmpzqWWCLo1QphjMTY6aKcrkCKs6sk9ewRxGVwi3j8DB4XaE9Vu5oSTWJAdk9TKV",
"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"
}
Response Fields
| Field | Type | Description |
|-------|------|-------------|
| verified | boolean | true if payment is confirmed and all checks pass |
| status | string | Transaction status (succeeded, pending, failed) |
| transaction_id | string | The transaction ID |
| tx_hash | string | Blockchain transaction hash |
| amount | string | Payment amount |
| currency | string | Currency code |
| customer_ref | string | null | Customer reference (if set when creating charge) |
| resource_ref | string | null | Resource reference (if set when creating charge) |
| reference | string | null | Optional reference string |
| confirmed_at | string | ISO 8601 timestamp when payment was confirmed |
Error Responses
Not Found (404)
Transaction doesn't exist or belongs to a different organization:
{
"detail": "Not Found"
}
Common causes:
- Transaction ID is incorrect
- Transaction was created with a different API key/organization
- Transaction was deleted
Conflict (409)
Transaction exists but verification failed:
{
"verified": false,
"reason": "customer_ref_mismatch",
"detail": "Transaction customer_ref does not match request",
"transaction_id": "3de45a76-4b5b-400f-9d40-7fee8557e76d"
}
Possible reason values:
| Reason | Description |
|--------|-------------|
| status_not_succeeded | Payment hasn't completed yet (still pending) or failed |
| customer_ref_mismatch | The customer_ref in the request doesn't match the transaction |
| resource_ref_mismatch | The resource_ref in the request doesn't match the transaction |
Unauthorized (401)
Invalid or missing API key:
{
"detail": "Invalid API key"
}
Verification Logic
The endpoint performs these checks in order:
1. Transaction exists?
└── No → 404 Not Found
└── Yes → Continue
2. Transaction belongs to your organization?
└── No → 404 Not Found (security: don't reveal existence)
└── Yes → Continue
3. Transaction status is "succeeded"?
└── No → 409 Conflict (reason: status_not_succeeded)
└── Yes → Continue
4. customer_ref matches (if provided in request)?
└── No → 409 Conflict (reason: customer_ref_mismatch)
└── Yes → Continue
5. resource_ref matches (if provided in request)?
└── No → 409 Conflict (reason: resource_ref_mismatch)
└── Yes → 200 OK (verified: true)
Security Considerations
Organization Isolation
The organization_id is determined by your API key, not the request body. This means:
- ✅ You can only verify transactions created by your organization
- ✅ Other organizations cannot verify your transactions
- ✅ Even if someone guesses a transaction ID, they can't verify it without your API key
Reference Matching
Use customer_ref and resource_ref to prevent:
- User A paying, User B accessing: Set
customer_refto the user ID - Paying for Resource A, accessing Resource B: Set
resource_refto the resource ID
Best Practices
- Always verify server-side - Never trust client claims about payment status
- Include both refs - Use
customer_refANDresource_reffor maximum security - Verify on every request - Don't cache verification results for too long
- Handle all error cases - 404 and 409 should both result in access denial
Integration Pattern
Recommended Flow
async function gateContent(userId, resourceId, transactionId) {
// 1. If no transaction ID, user hasn't paid
if (!transactionId) {
return { access: false, reason: 'no_payment' }
}
// 2. Verify the payment
const verification = await verifyPayment({
transaction_id: transactionId,
customer_ref: userId,
resource_ref: resourceId,
})
// 3. Check verification result
if (verification.verified) {
return {
access: true,
payment: verification
}
}
// 4. Verification failed
return {
access: false,
reason: verification.reason || 'verification_failed'
}
}
Caching Verification Results
For performance, you may cache successful verifications:
async function verifyWithCache(transactionId, userId, resourceId) {
// Check cache first
const cacheKey = `verify:${transactionId}:${userId}:${resourceId}`
const cached = await cache.get(cacheKey)
if (cached) {
return JSON.parse(cached)
}
// Verify with Meshpay
const result = await verifyPayment({
transaction_id: transactionId,
customer_ref: userId,
resource_ref: resourceId,
})
// Cache successful verifications (payments are immutable once succeeded)
if (result.verified) {
await cache.set(cacheKey, JSON.stringify(result), 'EX', 3600) // 1 hour
}
return result
}
Note: Only cache successful verifications. Failed verifications might succeed later (e.g., payment still pending).
Troubleshooting
"Not Found" when transaction exists
Cause: The transaction was created with a different API key/organization.
Solution: Use the same API key for creating and verifying charges.
"status_not_succeeded" immediately after payment
Cause: The facilitator hasn't confirmed the payment yet.
Solution: Wait a few seconds and retry. On-chain confirmation typically takes 1-30 seconds.
"customer_ref_mismatch" error
Cause: The customer_ref passed to verify doesn't match what was set when creating the charge.
Solution: Ensure you pass the exact same customer_ref to both create and verify.
Related Documentation
- Seller Integration Guide - Complete integration guide
- Webhooks - Asynchronous payment notifications
- Charges API - Creating charges
- Transactions - Listing and filtering transactions