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_ref and resource_ref match

Endpoint

POST
/v1/charges/verify

Authorization: Bearer <MESHPAY_API_KEY>

Content-Type: application/json


Request Body

FieldTypeRequiredDescriptionExample
transaction_idstring
Required
The transaction ID returned when the charge was created3de45a76-4b5b-400f-9d40-7fee8557e76d
customer_refstring | nullOptionalIf provided, verifies that the transaction's customer_ref matches this valueuser_123
resource_refstring | nullOptionalIf provided, verifies that the transaction's resource_ref matches this valuearticle:premium-guide-42

Code Examples

Basic Verification

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"
}
)
result = response.json()
print("Payment confirmed!" if result.get("verified") else "Payment not verified")

With Reference Validation

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"):
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_ref to the user ID
  • Paying for Resource A, accessing Resource B: Set resource_ref to the resource ID

Best Practices

  1. Always verify server-side - Never trust client claims about payment status
  2. Include both refs - Use customer_ref AND resource_ref for maximum security
  3. Verify on every request - Don't cache verification results for too long
  4. 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