Oxin Pay Integration Guide
Complete guide for integrating Oxin Pay into any project — e-commerce, SaaS, donations, POS, and more.
Table of contents
Complete reference in Markdown
The authoritative version of this guide (with full code samples, migration guides, and all 22 sections) is available as a Markdown file in the repository.
/var/www/oxinchain/oxin-pay/docs/OXIN_PAY_INTEGRATION_GUIDE.md1. Introduction
Oxin Pay accepts cryptocurrency payments on behalf of your customers. You create an invoice via API; the customer pays on a hosted checkout page; Oxin Pay sends a webhook when the payment confirms on-chain; you fulfill the order.
Oxin Pay is custodial — funds are held in Oxin Pay's custody and your merchant balance is credited (minus fee). You withdraw to your own wallets through the dashboard. You do not manage private keys.
Supported chains: BTC, LTC, DOGE, ETH, BSC, Polygon, Avalanche, Cronos, Tron, Solana, Oxin Chain — 39+ tokens total.
2. Quickstart (5 Minutes)
Step 1 — Get your API key
Sign up at pay.oxinchain.io → verify email → your key is emailed once. It starts with oxp_live_. New accounts get 100 test calls in pre-approval mode.
Step 2 — Create an invoice
USDT on BSC has token ID 20. Use it for testing:
curl -X POST https://pay.oxinchain.io/v1/invoices \
-H "Authorization: Bearer oxp_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"tokenId": 20,
"amountFiat": 1.00,
"fiatCurrency": "USD",
"merchantReference": "test-001",
"redirectUrl": "https://example.com/thank-you",
"cancelUrl": "https://example.com/cancel"
}'Step 3 — Redirect the customer
The response contains checkoutUrl. Redirect the customer there. The hosted page shows a QR code and deposit address.
Step 4 — Watch the status
curl https://pay.oxinchain.io/v1/invoices/INVOICE_PUBLIC_ID \ -H "Authorization: Bearer oxp_live_YOUR_KEY_HERE"
Status moves: created → pending → confirming → completed. See webhooks to be notified automatically.
3. Prerequisites for Production
- Merchant account approved by the Oxin Pay team
- API key approved (no pre-approval banner in responses)
- Chain and token access requested and approved (Settings → Payment methods)
- Withdrawal addresses whitelisted for each chain (24–48h review)
- Webhook endpoint registered at an HTTPS URL
- TOTP 2FA enabled on your merchant account (recommended)
4. Authentication
All merchant API calls use Bearer token authentication:
Authorization: Bearer oxp_live_YOUR_FULL_KEY_HERE
| State | API calls |
|---|---|
| pending_approval | Up to 100 lifetime test calls |
| active | Unlimited |
| disabled | Rejected (403) |
| revoked | Rejected (401) |
🔒 Store your API key in environment variables, never in source code. Never log it.
5. Supported Chains and Tokens
Fetch the live catalog — do not hardcode token IDs:
# All chains curl https://pay.oxinchain.io/v1/public/chains # All tokens (optional chainId filter) curl "https://pay.oxinchain.io/v1/public/tokens?chainId=5" # Your approved tokens curl https://pay.oxinchain.io/v1/currencies \ -H "Authorization: Bearer oxp_live_YOUR_KEY_HERE"
💡 For predictable pricing, use USDT or USDC on a fast-finality chain (BSC token ID 20, TRON token ID 36, Solana token ID 39). Confirmations in 1–12 seconds.
6. Core Concepts
Invoice lifecycle
created → pending → confirming → completed
↘ underpaid⚠️ Fulfill orders only on completed. Deposits in pending or confirming states are unconfirmed and can be reversed on some chains.
Fees
Default: 0.1% (10 bps) deducted from received amount. Each invoice shows feeBps, feeAmount, and netToMerchant.
Rate locking
Exchange rate is locked at invoice creation. amountCrypto does not change if the market moves. Rate risk is absorbed by the merchant.
7. Webhooks
Register a webhook URL in the dashboard (Settings → Webhooks). Oxin Pay POSTs to it when payment events occur.
Event types
| Event | When it fires |
|---|---|
| invoice.completed | Payment confirmed, merchant balance credited |
| invoice.underpaid | Deposit below required amount (after tolerance) |
| invoice.overpaid | Customer paid more than required; invoice still fulfilled |
Headers Oxin Pay sends
| Header | Value |
|---|---|
| X-Oxinpay-Event | e.g. invoice.completed |
| X-Oxinpay-Signature | sha256=<hex HMAC> |
| X-Oxinpay-Timestamp | Unix epoch string |
| X-Oxinpay-Delivery | Unique delivery ID for idempotency |
Signature verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, sigHeader) {
// rawBody must be Buffer — read before JSON parsing
const [algo, provided] = (sigHeader ?? '').split('=');
if (algo !== 'sha256' || !provided) return false;
const expected = crypto
.createHmac('sha256', process.env.OXIN_PAY_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
const a = Buffer.from(provided, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}🔒 Always use constant-time comparison (timingSafeEqual, secrets.compare_digest, hash_equals, etc.). Standard string equality (==) is vulnerable to timing attacks.
Retry policy
Up to 10 attempts with exponential backoff: min(5 × 2^(attempt−1), 86400) seconds. Timeout per attempt: 10 seconds. After 10 failures the delivery is marked dead.
📝 Oxin Pay guarantees at-least-once delivery. Deduplicate by X-Oxinpay-Delivery header before processing to avoid double fulfillment.
8. API Reference
All responses: { ok: true, data: {...}, request_id: "..." } or { ok: false, error: { code, message } }
/v1/healthService health check
/v1/public/chainsAll enabled chains
/v1/public/tokensAll enabled tokens (optional ?chainId=N)
/v1/public/ratesUSD prices for all tokens
/v1/currenciesYour approved chains and tokens
/v1/ratesExchange rates for your tokens
/v1/balancesYour merchant balance per token
/v1/invoicesList invoices (paginated, filterable)
/v1/invoicesCreate invoice, returns checkoutUrl
/v1/invoices/:publicIdGet single invoice detail
📝 Webhook management and withdrawals are handled through the merchant dashboard, not via the API. See Settings → Webhooks and Balances → Withdraw.
POST /v1/invoices — request fields
| Field | Required | Description |
|---|---|---|
| tokenId | Yes | Integer token ID from /v1/public/tokens |
| amountFiat | Yes | Payment amount in fiat (e.g. 49.00) |
| fiatCurrency | No | 3-letter code, default USD |
| description | No | Shown on checkout page, max 512 chars |
| merchantReference | No | Your internal order ID, max 128 chars |
| redirectUrl | No | URL after successful payment |
| cancelUrl | No | URL if customer cancels |
| metadata | No | Arbitrary JSON object stored on the invoice |
| ttlSeconds | No | Invoice lifetime in seconds (default ~3600) |
9. Integration Patterns
The full guide covers 9 patterns with working code snippets:
- E-commerce checkout (one-time purchase)
- SaaS subscription / account unlock (pay-per-period)
- Donation widget (variable amount)
- Marketplace / multi-vendor
- POS / in-person terminal
- Pay-per-API / metered credits
- Recurring / subscription-style (manual renewal reminder)
- Tipping / content monetization
- Game top-up / virtual currency
See the full Markdown guide for complete code per pattern: docs/OXIN_PAY_INTEGRATION_GUIDE.md § 9
10. Frontend Integration
Redirect flow (recommended)
// Your backend creates the invoice and returns checkoutUrl
document.getElementById('pay-btn').addEventListener('click', async () => {
const { checkoutUrl } = await fetch('/checkout/crypto', { method: 'POST' })
.then(r => r.json());
window.location.href = checkoutUrl;
});Success page — do not fulfill immediately on redirect
// Poll status — fulfillment happens via webhook
async function pollStatus(publicId) {
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
const { status } = await fetch(`/api/status/${publicId}`).then(r => r.json());
if (status === 'completed') { showSuccess(); return; }
if (status === 'underpaid') { showUnderpaid(); return; }
}
showPendingLong(); // "We'll email you when it confirms"
}11. Code Examples (5 Languages)
Runnable examples are in the repository at examples/:
Node.js
examples/nodejs/
npm install && node server.jsPython
examples/python/
pip install -r requirements.txt && python app.pyPHP
examples/php/
composer install && php -S localhost:8000 index.phpGo
examples/go/
go run main.goRuby
examples/ruby/
bundle install && ruby app.rbEach example includes a complete webhook handler with constant-time signature verification.
12. Error Reference
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid field — check message for field name |
| 401 | UNAUTHENTICATED | No or malformed Authorization header |
| 401 | api_key_not_found | Key does not exist |
| 401 | api_key_revoked | Key permanently revoked |
| 403 | api_key_disabled | Key is disabled |
| 403 | ip_blocked | Caller IP not in whitelist |
| 403 | quota_exceeded | Pre-approval 100-call cap reached |
| 403 | FORBIDDEN | Not permitted (e.g. unapproved token) |
| 404 | NOT_FOUND | Resource does not exist |
| 400 | TOKEN_DISABLED | Token disabled system-wide |
| 400 | INSUFFICIENT_BALANCE | Withdrawal exceeds balance |
| 429 | RATE_LIMITED | Too many requests — check Retry-After header |
| 500 | INTERNAL_ERROR | Unexpected error — retry with backoff |
13. Rate Limits
Rate limiting is per source IP. When exceeded, the response is 429 with a Retry-After header. Implement exponential backoff on 429 responses.
14. Security Best Practices
- Store API key and webhook secret in environment variables, never in source code
- Always verify webhook signatures — reject requests without a valid signature
- Read raw body bytes before JSON parsing, then verify, then parse
- Use constant-time comparison for signatures in every language
- Use HTTPS-only webhook URLs
- Whitelist your server's IPs on your API key
- Wrap order fulfillment in a DB transaction (atomic mark-paid + provision)
- Deduplicate webhook deliveries by
X-Oxinpay-Delivery - Rotate keys quarterly or after team member departures
15. Testing
There is no sandbox. Test with real small amounts — TRON USDT (token ID 36) costs ~$0.01 in fees and confirms in ~3 seconds.
Local webhook testing
ngrok http 3000 # Register the ngrok URL in Settings → Webhooks
Replay a webhook manually
SECRET="your_secret"
BODY='{"event":"invoice.completed","data":{"invoiceId":"TEST"},"timestamp":"2026-06-22T12:00:00Z"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3000/webhook \
-H "X-Oxinpay-Signature: sha256=$SIG" \
-H "X-Oxinpay-Event: invoice.completed" \
-H "X-Oxinpay-Delivery: test-001" \
-H "Content-Type: application/json" \
-d "$BODY"16. Going Live Checklist
- API key approved (no X-Oxin-Pay-Approval: pending header)
- Chain and token access approved in dashboard
- Webhook endpoint registered with HTTPS URL
- Signature verification implemented and tested with bad signatures
- Webhook idempotency implemented (same delivery → processed once)
- Webhook handler responds within 10 seconds
- DB transaction wraps order fulfillment
- At least one real-money end-to-end test passed
- Withdrawal addresses whitelisted for each chain
- IP whitelist configured on API key
- Monitoring on webhook delivery failures
- Customer support process for 'paid but order not showing'
17. Common Pitfalls
- Parsing JSON before signature verification (breaks HMAC byte-for-byte match)
- Using
==instead of constant-time comparison for signatures - Not deduplicating webhook deliveries → double fulfillment
- Fulfilling on
pendingorconfirming→ unconfirmed deposit - Not handling
underpaid→ stuck invoices requiring support - Polling for status instead of using webhooks → hits rate limits
- Hardcoding token IDs → breaks when tokens are added
- Skipping the real-money test → misses chain-specific issues
18. FAQ
Q: What is the difference between pending and completed?
A: pending = deposit detected, zero confirmations. completed = confirmation threshold reached, balance credited. Only fulfill on completed.
Q: My customer says they paid but the invoice is still pending. What do I do?
A: Check the blockchain explorer for the deposit address. If the transaction is on-chain, it is still accumulating confirmations — it will update automatically. Ask the customer for their transaction hash.
Q: Can I refund a customer?
A: Oxin Pay does not process automated refunds. Withdraw funds from your balance and send the customer their crypto manually, or offer store credit.
Q: Can I create an invoice without specifying a token?
A: No. Each invoice targets one specific token. Show a token picker in your UI using GET /v1/currencies, then create the invoice with the customer's chosen tokenId.
Q: What if my webhook server is down?
A: The dispatcher retries up to 10 times over ~21 minutes with exponential backoff. After that, deliveries are marked dead. You can replay them from the webhook delivery log in the dashboard, or check invoice status via the API.
Q: Do I need to KYC my customers?
A: Oxin Pay does not impose customer KYC. Your own compliance obligations depend on your jurisdiction. Consult a legal advisor.
See the full guide for 15+ additional FAQ entries, migration guides from Stripe / Coinbase Commerce / NOWPayments / BTCPay / PayPal, a complete glossary, and the versioning policy.
19–22. Migration, Glossary, Versioning, Support
The full Markdown guide covers migration from Stripe, Coinbase Commerce, NOWPayments, BTCPay Server, and PayPal; a 30-term glossary; the versioning policy (v1 is current, breaking changes introduce v2); and support contacts.
docs/OXIN_PAY_INTEGRATION_GUIDE.md
Full guide · ~12,000 words · 22 sections
Ready to integrate?
Start with the 5-minute quickstart, then register a webhook to receive real-time payment events.