Integrate stablecoin QR payments
Accept USDC & USDT on Solana with a single API call. Non-custodial, no smart contract, confirmed on-chain in about a second. This guide takes you from zero to a live payment — copy, paste, ship.
https://api.duitq.comOverview
DuitQ turns any invoice into a Solana Pay QR code. A customer scans it from their wallet, sends stablecoins straight to your wallet, and you get a confirmed payment webhook seconds later. There is no custody and no smart contract — matching happens through protocol-native Solana Pay reference keys.
Non-custodial
Funds settle payer → merchant directly. DuitQ never holds your money.
~1s confirmation
Payments detected on-chain and pushed to you over WebSocket or webhook.
USDC & USDT
6-decimal stablecoins on Solana. Price in USD or Indonesian Rupiah.
Authentication
Programmatic requests use an API key. You get one at registration — it is shown only once, so store it securely. Pass it as a bearer token:
Authorization: Bearer sk_live_a1b2c3d4e5f6...Or with the dedicated header:
X-API-Key: sk_live_a1b2c3d4e5f6...Quick start
Five steps from a fresh account to a paid invoice.
Register a merchant
curl -X POST https://api.duitq.com/api/merchants \
-H 'Content-Type: application/json' \
-d '{"name":"Kopi Kenangan","email":"ops@kopi.example"}'The response includes your apiKey (shown once) and a WhatsApp verification link.
Verify via WhatsApp
Open the returned whatsappUrl and send the prefilled code. Your account activates automatically.
Link a settlement wallet
# a) Get the challenge message to sign
curl "https://api.duitq.com/api/merchants/settlement/challenge?address=$WALLET" \
-H "Authorization: Bearer $API_KEY"
# b) Sign it (ed25519), base58-encode, then submit
curl -X PUT https://api.duitq.com/api/merchants/settlement \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"address":"<WALLET>","signature":"<base58-sig>"}'Provision token accounts (ATA)
# Auto-provision any missing ATAs (subsidized by the platform)
curl -X POST https://api.duitq.com/api/merchants/settlement/ata \
-H "Authorization: Bearer $API_KEY"Create your first invoice
Jump to Create an invoice — you are ready to take money.
SDK (Node / TypeScript)
The official @duitq/sdk wraps the REST API and webhook signature verification so you can integrate in a few lines. Works in Node 18+ and modern browsers (uses the global fetch).
npm install @duitq/sdkCreate & track an invoice
import { DuitQ } from '@duitq/sdk';
const duitq = new DuitQ({ apiKey: process.env.DUITQ_API_KEY });
// Create an invoice
const invoice = await duitq.createInvoice({
orderId: `ORD-${Date.now()}`,
amount: 12.5,
description: '2x Americano',
acceptedTokens: ['USDC', 'USDT'],
});
console.log(invoice.payments?.[0].qr); // data:image/png;base64,...
// Poll status
const latest = await duitq.getInvoice(invoice.id);
if (latest.status === 'PAID') {
// fulfil the order
}Verify webhooks
import { parseWebhook } from '@duitq/sdk';
import express from 'express';
const app = express();
// Use the RAW body so the HMAC matches
app.post('/webhooks/duitq', express.raw({ type: '*/*' }), (req, res) => {
try {
const event = parseWebhook({
secret: process.env.DUITQ_WEBHOOK_SECRET!,
timestamp: req.header('X-DuitQ-Timestamp')!,
rawBody: req.body.toString('utf8'),
signature: req.header('X-DuitQ-Signature')!,
});
if (event.event === 'invoice.paid') {
// mark order as paid — event.invoiceId, event.signature
}
res.json({ ok: true });
} catch {
res.status(400).end(); // invalid signature
}
});Configuration
new DuitQ({
apiKey: 'sk_live_...', // required for authenticated calls
baseUrl: 'https://api.duitq.com', // override per environment (e.g. staging)
});Exposes DuitQ (client), verifyWebhookSignature / parseWebhook, and fully-typed Invoice / WebhookPayload types.
Create an invoice
A single POST /api/invoices returns the invoice plus a ready-to-render QR (base64 PNG) and a solana: pay URL for each accepted token.
curl -X POST https://api.duitq.com/api/invoices \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"orderId": "ORD-20240115-001",
"amount": 12.50,
"description": "2x Americano, 1x Croissant",
"acceptedTokens": ["USDC", "USDT"],
"ttlMinutes": 15
}'Response (trimmed):
{
"id": "b1c2d3e4-...",
"orderId": "ORD-20240115-001",
"amount": 12.50,
"amountToken": 12.50,
"acceptedTokens": ["USDC", "USDT"],
"status": "PENDING",
"reference": "base58-reference-key",
"expiresAt": "2024-01-15T10:30:00.000Z",
"payments": [
{ "token": "USDC", "url": "solana:...", "qr": "data:image/png;base64,..." },
{ "token": "USDT", "url": "solana:...", "qr": "data:image/png;base64,..." }
]
}Prefer a standalone image? Fetch GET /api/invoices/:id/qr?token=USDC&format=png (or format=svg).
Track payment status
Three ways to know the moment a payment lands.
WebSocket (recommended)
import { io } from "socket.io-client";
const socket = io("wss://api.duitq.com/ws");
socket.emit("subscribe", { invoiceId: "b1c2d3e4-..." });
socket.on("payment", (data) => {
if (data.status === "PAID") console.log("Confirmed!", data.signature);
});Polling
curl https://api.duitq.com/api/invoices/$INVOICE_ID
# => { "status": "PENDING" | "PAID" | "EXPIRED" | "NEEDS_REVIEW" }For server-to-server reliability, use webhooks — they retry until acknowledged.
IDR pricing
Price in Indonesian Rupiah and DuitQ locks the token amount from a live CoinGecko rate (with a platform spread) at invoice creation.
# Live FX quote
curl "https://api.duitq.com/api/fx/quote?fiat=IDR&token=USDT"
# => { "midRate": 15850.25, "effectiveRate": 15691.75, "spreadBps": 100 }
# IDR-priced invoice
curl -X POST https://api.duitq.com/api/invoices \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"orderId":"ORD-IDR-001","amount":150000,"pricingCurrency":"IDR","acceptedTokens":["USDT"]}'The amountToken in the response is the exact USDT the payer sends.
Webhooks
Register a URL to receive real-time payment events.
curl -X PUT https://api.duitq.com/api/merchants/webhook \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url":"https://your-app.com/webhooks/duitq"}'
# => { "webhookSecret": "whsec_abc123..." } (shown once)Events
| Event | Trigger |
|---|---|
invoice.paid | Payment confirmed on-chain |
invoice.expired | TTL elapsed without payment |
invoice.needs_review | Double payment or amount mismatch |
Verify the signature
`${timestamp}.${rawBody}` using your webhookSecret.import crypto from "node:crypto";
function verify(req, rawBody, secret) {
const ts = req.headers["x-duitq-timestamp"];
const sig = req.headers["x-duitq-signature"];
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Failed deliveries retry with exponential backoff (5s base, 1h cap) up to 8 attempts. Deduplicate with the X-DuitQ-Idempotency-Key header.
Prepaid credits
A non-custodial prepaid model: 50 free credits on signup. Creating an invoice costs 1 credit; a settled invoice costs 1 more (2 total per paid invoice).
# Check balance
curl https://api.duitq.com/api/merchants/credits -H "Authorization: Bearer $API_KEY"
# Buy a pack (creates a Solana Pay invoice to the platform wallet)
curl -X POST https://api.duitq.com/api/merchants/credits/order \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"packs":1}'Invoice lifecycle
| Status | Meaning |
|---|---|
| PENDING | Awaiting payment, QR is active |
| PAID | Payment confirmed on Solana |
| EXPIRED | TTL elapsed without payment |
| NEEDS_REVIEW | Double payment or amount mismatch |
| REFUNDED | Manually marked refunded by the merchant |
Errors & limits
Errors follow a consistent shape:
{ "statusCode": 400, "message": "Descriptive error", "error": "Bad Request" }| Status | Meaning |
|---|---|
400 | Validation error or bad request |
401 | Missing or invalid authentication |
402 | Insufficient credits |
404 | Resource not found |
429 | Rate limit exceeded (120 req/min per IP) |
500 | Internal server error |
