Integration Guide
Add stablecoin checkout to your website.
Include one script, configure your wallet, and start accepting payments. No backend integration required.
Prerequisites
Set up your wallet to receive payments.
One-time setup. Your wallet address establishes the settlement endpoint for all USDC transactions.
Quick start
Three steps to accepting payments.
No backend integration required. Add the script, configure your wallet, and create payment buttons.
Include the script
Add this line just before the closing </body> tag.
<script src="https://app.instantescrow.nz/conduit-checkout.js"></script>Initialize with your wallet
Configure the checkout with your merchant wallet address.
<script>
ConduitCheckout.init({
// REQUIRED: Your wallet address to receive payments
sellerAddress: '0xYourWalletAddressHere',
// REQUIRED: Base URL of checkout page
baseUrl: 'https://app.instantescrow.nz',
// RECOMMENDED: Auto-send verified payment to your backend
webhookUrl: 'https://yoursite.com/api/conduit-webhook',
webhookSecret: 'your-secret-key', // Optional. Not the check that matters — see below.
// Optional: Default token ('USDC' or 'USDT')
tokenSymbol: 'USDC',
// Optional: Days until auto-release (default: 7)
expiryDays: 7,
// Optional: Display mode ('popup' or 'redirect')
mode: 'popup',
// Success callback (webhook already sent!)
onSuccess: function(data) {
console.log('Payment verified!', data);
alert('Thank you! Order #' + data.orderId + ' confirmed!');
},
// Error callback
onError: function(error) {
console.error('Payment failed:', error);
alert('Payment failed: ' + error);
},
// Cancel callback
onCancel: function() {
console.log('Payment cancelled');
}
});
</script>Add payment buttons
Create buttons that open the checkout.
<button onclick="ConduitCheckout.open({
amount: '50.00',
description: 'Premium Product'
})">
Pay $50 with USDC
</button>Display modes
Choose how checkout appears.
Popup window
Opens in a centered popup. Best for minimal disruption to the shopping experience.
mode: 'popup'Examples
Common integration patterns.
Basic product payment
Simple checkout for a single product with order tracking.
<button onclick="ConduitCheckout.open({
amount: '29.99',
description: 'Premium Widget - Blue',
orderId: 'ORDER-12345'
})">
Buy Now - $29.99
</button>Reference
Configuration options.
ConduitCheckout.init(options)
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| sellerAddress | string | Yes | - | Your wallet address to receive payments |
| baseUrl | string | Yes | - | Base URL of checkout page |
| webhookUrl | string | No | - | Webhook URL for payment verification |
| webhookSecret | string | No | - | Optional HMAC secret for webhook signatures. Set in the browser, so confirm against /api/results before fulfilling |
| tokenSymbol | string | No | 'USDC' | 'USDC' or 'USDT' |
| expiryDays | number | No | 7 | Days until auto-release to seller |
| mode | string | No | 'popup' | 'popup' or 'redirect' |
| onSuccess | function | No | - | Callback when payment completes |
| onError | function | No | - | Callback when payment fails |
| onCancel | function | No | - | Callback when user cancels |
ConduitCheckout.open(params)
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | string/number | Yes | Payment amount (e.g., '50.00') |
| description | string | Yes | Payment description (max 160 chars) |
| orderId | string | No | Your internal order/transaction ID |
| string | No | Customer email address | |
| tokenSymbol | string | No | Override default token for this payment |
| expiryDays | number | No | Override default expiry |
| webhookUrl | string | No | Override webhook URL for this payment |
| metadata | object | No | Custom metadata to include |
Backend integration
Webhook-based order fulfillment.
Get told when a payment completes, then confirm it yourself. The webhook says something happened; /api/results is the source of truth, and it is a public endpoint you can query from your server with no key and no setup.
Configure SDK with webhook
The SDK handles verification and webhook delivery automatically. No manual fetch() calls needed.
ConduitCheckout.init({
sellerAddress: '0xYourWalletAddress',
baseUrl: 'https://app.instantescrow.nz',
// Webhook config
webhookUrl: 'https://yoursite.com/api/conduit-webhook',
// Optional. It signs the POST, but this file is served to every visitor, so treat
// the signature as a hint and let the /api/results lookup below decide.
webhookSecret: 'your-secret-key',
onSuccess: function(verifiedData) {
// Webhook already sent! Just show UI confirmation
console.log('Payment verified!', verifiedData);
window.location.href = '/thank-you?order=' + verifiedData.orderId;
},
onError: function(error) {
alert('Payment failed: ' + error);
}
});Confirm it, then fulfil
Ask /api/results what really happened before you ship anything. It reads the same record the checkout polls, so it settles the amount, the currency and who was paid — and a request that never arrives changes nothing.
// POST /api/conduit-webhook
app.post('/api/conduit-webhook', async (req, res) => {
try {
const { contractId, orderId } = req.body;
// 1. ASK THE SOURCE OF TRUTH. Never fulfil on the request body alone —
// this is the only thing here that your own server has checked.
const lookup = await fetch('https://api.stabledrop.me/api/results', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contractid: contractId })
});
const { count, results } = await lookup.json();
if (count === 0) {
return res.status(404).json({ error: 'No such payment' });
}
const payment = results[0];
// 2. CHECK IT IS THE PAYMENT YOU WERE EXPECTING
const order = await db.orders.findUnique({ where: { id: orderId } });
// `state` is what says the money arrived. List the states that mean it did,
// rather than ruling out the ones that mean it did not: a state you have not
// heard of must not be read as payment. Waiting on one that turns out to be
// fine costs another poll; clearing one that is not costs you the goods.
//
// ⚠️ chainAddress is NOT that signal. An escrow's address is worked out from
// its terms and recorded when the buyer opens the payment page, before they
// send anything — so it is present on unpaid orders too.
const FUNDED_STATES = ['ACTIVE', 'DISPUTED', 'RESOLVED', 'CLAIMED', 'COMPLETED'];
const paidToYou = payment.sellerWalletId.toLowerCase() === YOUR_WALLET.toLowerCase();
const amount = payment.currency.toLowerCase().startsWith('micro')
? Number(payment.amount) / 1000000
: Number(payment.amount);
if (!paidToYou) return res.status(400).json({ error: 'Not your wallet' });
if (!FUNDED_STATES.includes(payment.state)) return res.status(409).json({ error: 'Not funded yet' });
if (Math.abs(amount - order.total) > 0.001) {
return res.status(400).json({ error: 'Amount mismatch' });
}
// 3. NOW IT IS SAFE TO FULFIL
await db.orders.update({
where: { id: orderId },
data: {
status: 'PAID',
paymentContractId: contractId,
paymentChainAddress: payment.chainAddress,
paymentAmount: amount,
paidAt: new Date()
}
});
await fulfillment.ship(orderId);
await email.sendConfirmation(order.email, orderId);
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Check a payment yourself
The same query, on its own. Public, unauthenticated, no key to request — give it a contractid and it tells you what was paid and to whom. Useful for reconciliation, for support, and for replaying anything a webhook missed.
curl -X POST https://api.stabledrop.me/api/results \
-H 'Content-Type: application/json' \
-d '{"contractid": "507f1f77bcf86cd799439011"}'Response:
{
"count": 1,
"results": [
{
"contractid": "507f1f77bcf86cd799439011",
"chainAddress": "0x1234567890abcdef1234567890abcdef12345678",
"sellerWalletId": "0xYourWalletAddress",
"amount": 50000000.0,
"description": "Order 1234",
"currency": "microUSDC",
"state": "CLAIMED",
"chainId": "8453",
"createdate": 1705318200,
"maturity": 1705404600
}
]
}Query fields
| Field | Type | Description |
|---|---|---|
| contractid | string | Exact match on the contract id the checkout returns |
| chainAddress | string | Exact match on the escrow address |
| chainAddresses | string[] | The batch form. OR within the list, so one round trip fills in a whole page of contracts instead of one call each. Case-insensitive, because addresses read from event logs are lower case while records may be checksummed |
| sellerWalletId | string | Matches seller OR buyer, case-insensitively — this is how you list everything paid to your wallet |
All optional, but at least one is required — an empty body returns 400. Multiple fields combine with AND.
Response fields
| Field | Type | Description |
|---|---|---|
| contractid | string | The contract id |
| chainAddress | string | The escrow address. Assigned when the buyer opens the payment page, so its presence says nothing about whether they paid — read state for that |
| sellerWalletId | string | Who gets paid. Compare against your own wallet |
| amount | number | In the units of currency below, so divide by 1,000,000 for a micro currency |
| description | string | What the payment was for |
| currency | string | e.g. microUSDC. There is no separate currencySymbol field |
| state | string | Whether the money arrived. ACTIVE, DISPUTED, RESOLVED, CLAIMED and COMPLETED all mean it did; treat anything else as not yet paid, including states you do not recognise |
| chainId | string | e.g. 8453 for Base |
| createdate | number | Unix seconds when the record was made |
| maturity | number | Unix seconds when the cashflow unlocks. Distinct from createdate |
count: 0 means no such payment — treat it as unpaid, never as an error. You can also query by chainAddress, or by sellerWalletId to list everything paid to your wallet. Amounts in a micro-prefixed currency are in millionths, so divide by 1,000,000. state is the escrow's lifecycle, not a pass/fail — a completed payment reads CLAIMED, so check for the failure states rather than for one success value.
Webhook payload
{
"contractId": "507f1f77bcf86cd799439011",
"chainAddress": "0x5678...",
"seller": "0xYourWalletAddress",
"amount": 50.00,
"amountRaw": 50000000,
"currencySymbol": "USDC",
"currencyRaw": "microUSDC",
"description": "Order 1234",
"state": "OK",
"verified": true,
"verifiedAt": "2026-01-15T10:30:00.000Z",
"orderId": "1234",
"email": "buyer@example.com",
"metadata": {},
"timestamp": 1705318200
}Verification
What verified data means.
| Field | Meaning |
|---|---|
| verified: true | Backend confirmed payment exists in blockchain |
| state: "ACTIVE" | Funds are locked in escrow contract |
| seller: "0x..." | Matches your wallet (verified by SDK) |
| amount: 50.0 | Matches expected amount (verified by SDK) |
| chainAddress | Blockchain contract address (permanent record) |
Once you receive verified data, it is safe to fulfil the order. The SDK has confirmed everything on-chain.
Platform
Built-in protection for every transaction.
Buyer protection
- Funds held in escrow smart contract
- Time-delayed release (default 7 days)
- Dispute mechanism
- Admin arbitration
No gas fees
- Platform covers all blockchain fees
- Users pay 1% platform fee
- Minimum payment: $1.001
- Fee included in payment amount
Client-side security
- Users sign with own wallet
- No custody of user funds
- HTTPS required
- Open-source smart contracts
Multi-token support
- USDC (default)
- USDT (optional)
- Base network (Ethereum L2)
- Low transaction costs
End-to-end flow
Customer clicks pay
Opens checkout popup or redirect.
Wallet connects & pays
Customer signs the transaction.
SDK verifies on-chain
Automatic blockchain verification.
Webhook sent to backend
Your server receives verified data.
Order fulfilled
Ship goods, deliver product, send receipt.
FAQ
Frequently asked questions.
What tokens are supported?
What are the fees?
How long until I receive funds?
Can buyers get refunds?
Do I need a crypto wallet?
Is this secure?
What about chargebacks?
Ready to integrate?
Try the interactive demo or start integrating directly into your site.