The Desk API
Een REST API om kaarten en crypto te charten, met settlement naar een USDC-wallet die jij beheert. Eén POST maakt een checkout aan. Eén webhook vertelt je dat er is betaald. Ship in minder dan 30 minuten.
Aan de slag
The Desk ondersteunt vier integratiemodi. Kies degene die bij jouw beperkingen past; de onderliggende API is dezelfde.
Drop-in knop, geen signup. Pass je USDC wallet-adres in de request body. Nul backend state; publiek, zichtbaar in DevTools.
Server-to-server. Verberg je wallet achter Bearer sk_live_. Configureer branding, webhooks, mass payouts vanuit het dashboard.
Kant-en-klare plugin. Upload de ZIP, plak je API-key + webhook secret, orders worden automatisch afgerond bij betaling. HPOS-ready, WC 7.0+.
Custom App + Handmatige Betaalmethode. ~30 min installatie op een bestaande winkel. We markeren bestellingen als betaald via de Shopify Admin API.
Quickstart (5 min)
Drie stappen: maak een sessie aan, redirect de klant, handle de webhook. Het voorbeeld hieronder is een production-ready Node.js checkout-route.
// Create a checkout session and redirect your customer.
// Authorization resolves the merchant wallet server-side — no wallet in the body.
const res = await fetch('https://pay.qistdigital.com/api/v1/checkout/init', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.QIST_API_KEY}`, // sk_live_…
},
body: JSON.stringify({
amount_cents: 5000, // €50.00 — integer, in cents
currency: 'EUR', // EUR, USD, GBP, CAD, AUD, CHF
email: 'buyer@example.com', // optional, shown in checkout
success_url: 'https://mystore.com/success',
cancel_url: 'https://mystore.com/cart',
webhook_url: 'https://mystore.com/api/qist-webhook',
metadata: { order_id: '1234' },
}),
});
const { id, url, tracking_number } = await res.json();
// => { id: 'cs_abc…', url: 'https://pay.qistdigital.com/session/cs_abc…',
// tracking_number: '0x…', provider: 'gateway', status: 'pending', … }
// Redirect your customer to the hosted checkout.
return Response.redirect(url, 303);Dat is het hele happy path. De klant landt op een hosted checkout op , kiest kaart of crypto, en je webhook vuurt binnen 30s na betaling.
Authenticatie
Twee schema's, afhankelijk van de integratiemodus:
| Schema | Hoe | Wanneer |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Server-side. Houdt de wallet privé. |
| Wallet in body | { "wallet": "0x…", … } | Statische sites / widgets zonder backend. |
sk_live_… and an sk_test_… key. Use sk_live_ as your canonical key — that is what every example here uses. The sk_test_ key is provided for the webhook-receiver simulator at /api/v1/test/fire-webhook. The Desksettles real on-chain USDC — there is no test network. To dry-run an integration, run a $1 real payment and refund yourself.API-referentie
Base URL: . Alle endpoints spreken JSON, geven één object terug bij success, en een -object bij 4xx/5xx.
#Maak een checkout-sessie aan
Genereert een hosted checkout URL. De klant opent hem, betaalt met kaart of crypto, The Desk settelt naar je wallet in USDC, webhook vuurt.
| Veld | Type | Vereist | Omschrijving |
|---|---|---|---|
| amount_cents | integer | vereist | Bedrag in de kleinste valuta-eenheid (cents). Bereik 100 – 10 000 000. |
| currency | string | vereist | ISO 4217-code. Ondersteund: EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | one-of | USDC-wallet op Polygon (0x + 40 hex). Vereist tenzij geauthenticeerd via Bearer-key. |
| customer_email | string | optioneel | Getoond in de checkout UI en doorgestuurd naar de on-ramp voor KYC-hergebruik. |
| success_url | url | optioneel | Redirect na succesvolle betaling. Alleen http/https. |
| cancel_url | url | optioneel | Redirect als de klant de checkout verlaat. |
| webhook_url | url | optioneel | POST-target voor order.paid events. Overschrijft de dashboard-default. |
| provider | string | optioneel | Default 'gateway' (smart picker — aanbevolen). Of pin een specifieke on-ramp id uit GET /providers (bv. moonpay, revolut, banxa, transak). |
| product_name | string | optioneel | Label getoond op de checkout-pagina (max 80 tekens). |
| metadata | object | optioneel | Tot 10 string key/value pairs, echoed terug in de webhook. Gereserveerde key: order_id. |
| Veld | Type | Vereist | Omschrijving |
|---|---|---|---|
| id | string | optioneel | Sessie-id, begint met cs_. |
| url | string | optioneel | Hosted checkout URL om de klant naar te redirecten. |
| status | string | optioneel | Altijd "pending" bij creatie. |
| amount | integer | optioneel | Echo van amount_cents. |
| currency | string | optioneel | Echo van currency. |
| provider | string | optioneel | Echo van provider (default 'gateway'). |
| expires_at | string | optioneel | ISO 8601 expiry (24u vanaf creatie). |
| tracking_number | string | optioneel | Polygon settlement-adres — matcht address_in in de webhook payload, bruikbaar met /track voor live monitoring. |
Voorbeelden
// Node.js 18+
const res = await fetch('https://pay.qistdigital.com/api/v1/checkout/init', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.QIST_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(), // safe double-submit
},
body: JSON.stringify({
amount_cents: 5000,
currency: 'EUR',
email: 'buyer@example.com',
success_url: 'https://mystore.com/success',
cancel_url: 'https://mystore.com/cart',
metadata: { order_id: '1234' },
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { id, url } = await res.json();
return Response.redirect(url, 303);#Een sessie ophalen (polling)
Gebruik dit als webhook-fallback, of om een success-pagina te hydrateren na redirect. Server-side re-checkt onze settlement-laag bij elke call — goedkoop (<200ms), dus elke 3-5s pollen is prima.
| Veld | Type | Vereist | Omschrijving |
|---|---|---|---|
| id | string | optioneel | Sessie-id. |
| status | string | optioneel | pending | paid | expired | failed. |
| amount | integer | optioneel | Oorspronkelijk bedrag in cents. |
| currency | string | optioneel | Oorspronkelijke valuta. |
| paid_at | string|null | optioneel | ISO 8601 wanneer de on-chain settlement voltooide. |
| paid_provider | string|null | optioneel | Welke provider daadwerkelijk de betaling heeft verwerkt (kan verschillen van de gevraagde). |
| txid | string|null | optioneel | Polygon settlement txid. Link met polygonscan.com/tx/{txid}. |
| expires_at | string | optioneel | ISO 8601 expiry. |
// Poll every 3-5 seconds until terminal state. Use webhooks for push-
// delivery in production; polling is the fallback when webhooks are down.
async function waitForPayment(sessionId, { timeoutMs = 15 * 60 * 1000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`https://pay.qistdigital.com/api/v1/sessions/${sessionId}`);
const s = await res.json();
if (s.status === 'paid') return s; // terminal: success
if (s.status === 'expired') throw new Error('Session expired');
if (s.status === 'failed') throw new Error('Payment failed');
await new Promise(r => setTimeout(r, 4000));
}
throw new Error('Polling timeout');
}#Live provider-matrix
Lijst van de on-ramps die momenteel traffic accepteren, met per-provider minimumbedragen. 5 min gecached aan de edge — poll één keer bij app-boot, niet per request.
| Veld | Type | Vereist | Omschrijving |
|---|---|---|---|
| providers[].id | string | optioneel | Provider-key (door te geven als `provider` in /checkout/init). |
| providers[].provider_name | string | optioneel | Menselijk label voor de dropdown. |
| providers[].status | string | optioneel | 'active' (altijd gefilterd op active bij dit endpoint). |
| providers[].minimum_currency | string | optioneel | ISO-code van het minimum. |
| providers[].minimum_amount | number | optioneel | Laagste bedrag dat de provider accepteert (in minimum_currency-eenheden). |
curl -sS 'https://pay.qistdigital.com/api/v1/providers' | jq '.providers[] | {id, provider_name, minimum_currency, minimum_amount}'
# [
# { "id": "gateway", "provider_name": "Smart (recommended)", "minimum_currency": "USD", "minimum_amount": 1 },
# { "id": "moonpay", "provider_name": "Moonpay", "minimum_currency": "EUR", "minimum_amount": 20 },
# { "id": "revolut", "provider_name": "Revolut Ramp", "minimum_currency": "EUR", "minimum_amount": 10 },
# { "id": "binance", "provider_name": "Binance Pay", "minimum_currency": "EUR", "minimum_amount": 15 },
# …
# ]
# Cache: 5 minutes at the edge. Call once per deploy, not per request.Webhooks
Wanneer een sessie een terminale status bereikt, POSTen we een getekend JSON-event naar de die je hebt geconfigureerd (per sessie of in het dashboard). Parse altijd de request body voor signature-verificatie — JSON opnieuw serialiseren herordent keys en breekt de HMAC.
POST /your-endpoint HTTP/1.1
Host: mystore.com
Content-Type: application/json
x-peptidepay-signature: t=1745300551,v1=3f9b5c1e8a7d… ← HMAC-SHA256, hex
{
"event": "order.paid",
"session_id": "cs_abc123",
"order_id": "1234",
"address_in": "0xAb12…",
"status": "paid",
"amount": 5000,
"currency": "EUR",
"txid": "0xfa89b2…",
"paid_at": "2026-04-23T10:02:31.000Z",
"attempt": 1
}Event types
| Event | Wanneer |
|---|---|
| order.paid | On-chain settlement bevestigd. + + zijn gegarandeerd aanwezig. Markeer de order als betaald. |
Alleen wordt vandaag geleverd — verlopen en mislukte sessies zijn zichtbaar via (status gaat naar na de 24u TTL; terminale failures tonen ). We voegen mogelijk push-events toe voor die in een toekomstige release.
Signature-verificatie
Merchants met een signup-account krijgen een secret en elke delivery draagt een header in de vorm . Bereken en doe een constant-time compare met . Weiger alles ouder dan 5 minuten.
// Node.js — Express/Next.js route handler
import crypto from 'node:crypto';
const SECRET = process.env.QIST_WEBHOOK_SECRET; // dashboard → Webhooks
export async function POST(req) {
const rawBody = await req.text(); // MUST be the raw bytes
const header = req.headers.get('x-peptidepay-signature') ?? '';
const [ tPart, v1Part ] = header.split(',');
const t = tPart?.split('=')[1];
const v1 = v1Part?.split('=')[1];
if (!t || !v1) return new Response('bad sig', { status: 400 });
// Reject replays older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300)
return new Response('stale', { status: 400 });
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${rawBody}`)
.digest('hex');
const ok =
v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
if (!ok) return new Response('invalid sig', { status: 401 });
const event = JSON.parse(rawBody);
// Idempotency: dedupe by event.session_id in your DB — retries re-fire
// the same event (with an incrementing "attempt" field) until you 2xx.
if (event.event === 'order.paid') {
await markOrderPaid(event.order_id, event.txid);
}
return new Response('ok');
}Retry policy
We retryen non-2xx responses (en timeouts > 5s) op een exponentieel backoff. Zes totale pogingen over ~42 uur:
- Poging 1 — meteen bij bevestiging.
- Poging 2 — +5 minuten.
- Poging 3 — +15 minuten.
- Poging 4 — +1 uur.
- Poging 5 — +4 uur.
- Poging 6 — +12 uur, dan +24 uur (finaal).
Na 6 mislukte pogingen wordt het event dead-lettered. Je kunt de huidige state op elk moment opnieuw opvragen via .
Veelvoorkomende issues
- Ik zie 'invalid signature' op elke delivery
- Je framework parsete de body als JSON voordat je hashte. Lees de RAW bytes (Express: express.raw({type:'*/*'}); Next.js: req.text(); Laravel: request()->getContent(); Rails: request.raw_post). Nooit opnieuw serialiseren voor het hashen.
- IP-whitelist — vanaf welke IPs versturen jullie?
- Deliveries komen momenteel van Vercel Edge (dynamische IPs). We publiceren geen statische range. Als je MOET whitelisten, gebruik de signature-header als je auth-gate en accepteer elke source IP — de HMAC is de echte identiteitscheck.
- HTTPS vereist?
- Ja. We weigeren te POSTen naar http:// endpoints (confusable deputy / plaintext replay-risico). ngrok's gratis https-URL werkt prima voor lokaal testen.
- Mijn endpoint is traag — kan ik de 5s timeout verlengen?
- Nee. Reageer meteen 2xx, verwerk daarna asynchroon (job queue, setImmediate, goroutine). Lang blokkerende handlers timen altijd uit.
SDKs
De API is klein genoeg dat perfect werkt — maar de Node SDK geeft je types, automatische retries, en een -helper die signature-verificatie voor je regelt.
// npm install github:kinerette/peptide-pay-sdk
// The published package is still named "peptide-pay"; we alias the client
// to Qist here so your code reads cleanly.
import { PeptidePay as Qist } from 'peptide-pay';
const qist = new Qist(process.env.QIST_API_KEY);
// Create a session
const session = await qist.checkout.create({
amount_cents: 5000,
currency: 'EUR',
customer_email: 'buyer@example.com',
success_url: 'https://mystore.com/success',
cancel_url: 'https://mystore.com/cart',
metadata: { order_id: '1234' },
});
// Retrieve a session
const latest = await qist.sessions.retrieve(session.id);
// Verify + parse a webhook (throws on invalid signature)
app.post('/webhooks/qist', express.raw({ type: '*/*' }), (req, res) => {
const event = qist.webhooks.constructEvent(
req.body,
req.headers['x-peptidepay-signature'], // signature header is unchanged
process.env.QIST_WEBHOOK_SECRET,
);
// event.event === 'order.paid' (currently the only event delivered)
res.sendStatus(200);
});The DeskVolledige types, webhook-helper, automatische retries.
elke taalEén POST, één GET. Geen library nodig.
Tarieven
Flat — de volledige commissie van The Desk. Geen abonnement, geen maandelijkse fee, geen chargeback fees. Card on-ramp fees (~4.5% door de upstream card processor aangerekend) zijn pass-through — de klant betaalt ze, ze raken nooit je payout.
| Betaalmethode | Jij betaalt | Klant betaalt |
|---|---|---|
| Kaart / Apple Pay / Google Pay | 3% | ~4.5% (on-ramp, pass-through) |
| Crypto direct (USDC → USDC) | 3% | alleen gas (~$0.01 op Polygon) |
Volledige breakdown met uitgewerkte voorbeelden op /fees.
Testen
The Desk heeft geen sandbox-netwerk — elke settlement is een echte on-chain transfer. De test met het laagste risico is een die je aan jezelf terugbetaalt. Om je webhook end-to-end te testen zonder te betalen, gebruik je de test-simulator hieronder.
- Webhook-simulator: POST naar /api/v1/test/fire-webhook met een sk_test_-sleutel om een gesigneerde order.paid naar je endpoint te vuren — geen betaling, geen refund nodig.
- MoonPay dev card: , elke toekomstige expiry, elke CVV, ZIP 10001.
- Lokaal webhook testen: expose localhost met ngrok, plak de URL in het -veld per sessie.
Volledige lokale loop (ngrok)
# Test your webhook receiver WITHOUT a real on-ramp payment.
# Do NOT test by paying a tiny amount: anything below an on-ramp's
# minimum (~$20) hits the gateway's "below minimum" error + KYC — that's
# the live on-ramp, not your webhook. Use the signed simulator instead.
# 1. Expose your local webhook endpoint
ngrok http 3000
# 2. Create a TEST session with your sk_test_ key. Amount can be anything
# (the simulator ignores on-ramp minimums). Note the "id" it returns.
curl -X POST 'https://pay.qistdigital.com/api/v1/checkout/init' \
-H "Authorization: Bearer $QIST_TEST_KEY" \
-H 'Content-Type: application/json' \
-d '{
"amount_cents": 5000,
"currency": "EUR",
"customer_email": "test+sandbox@yours.com",
"webhook_url": "https://xxxx.ngrok-free.app/webhooks/qist"
}'
# 3. Fire a SIGNED order.paid straight at your endpoint — no on-ramp, no
# minimum, no KYC. (webhook_url_override also accepts localhost / ngrok
# / loca.lt / trycloudflare.com if you'd rather not set it on the session.)
curl -X POST 'https://pay.qistdigital.com/api/v1/test/fire-webhook' \
-H "Authorization: Bearer $QIST_TEST_KEY" \
-H 'Content-Type: application/json' \
-d '{ "session_id": "PASTE_id_FROM_STEP_2" }'
# 4. The response echoes your receiver's HTTP status + the exact signature
# header we sent, so you can line it up against your HMAC verification.Errors & rate limits
Alle errors delen de shape . Statuscodes zijn standaard REST.
400401403404429502Troubleshooting
- Sessie is 'paid' in het dashboard maar mijn webhook is nooit afgevuurd
- Check of webhook_url bereikbaar is over publieke HTTPS (curl hem van buiten je LAN). Als het klopt, poll GET /sessions/{id} om de status te bevestigen — het dashboard /app toont webhook delivery stats (success rate, counts). Zes pogingen over 42u voor dead-letter; je kunt altijd resyncen via polling.
- HMAC mismatch — signature is altijd invalid
- 99% van de tijd: je hasht een opnieuw geserialiseerde body in plaats van de raw bytes. Frameworks parsen JSON automatisch voordat je handler draait; je hebt de raw buffer nodig. Next.js: req.text() voor elke .json(). Express: app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails: request.raw_post. Check ook dat je `HMAC(whsec_secret, t + '.' + rawBody)` berekent — NIET gewoon `HMAC(whsec_secret, rawBody)`. De timestamp-prefix is vereist.
- MoonPay zegt 'service unavailable in your country'
- MoonPay beperkt ~20 landen (Iran, Noord-Korea, Cuba, volledige lijst op hun site). Default provider is 'gateway' — de smart picker valt automatisch terug op Revolut, Transak of Banxa, die andere geografieën dekken. Als je een specifieke provider hebt gepind met provider: 'moonpay', laat hem weg en laat de router kiezen.
- Mijn wallet heeft geen USDC ontvangen na een 'paid' event
- Check polygonscan.com/address/<your-wallet> voor USDC (Polygon POS) transfers. Settlement landt 97% bij jou en 3% bij The Desk — als je de 97% inbound niet ziet, heb je mogelijk de verkeerde wallet in de init-call geplakt. Herbevestig via GET /sessions/{id} — het txid-veld wijst naar de echte on-chain transfer.
- Klant werd twee keer afgeschreven
- Zou niet moeten gebeuren. Elke sessie heeft één settlement addressIn; een tweede betaling naar hetzelfde adres wordt aan onze kant een aparte sessie en we crediteren alleen de eerste aan je order. Als het gebeurt, screenshot de twee polygonscan-txids + de sessie-id en mail hi@pay.qistdigital.com — we refunden het duplicaat vanuit onze treasury.
- Ik krijg 502 'Payment infrastructure temporarily unavailable'
- Onze settlement-upstream is gedegradeerd (< 0.5% van requests). Retry in 30s met dezelfde Idempotency-Key — onze cache geeft de originele response terug zodra de wallet succesvol wordt gegenereerd. Volg /status voor live incidents.
Klaar om te integreren?
De meeste merchants gaan van nul naar hun eerste betaalde transactie in minder dan 30 minuten.