API The Desk
REST API do obsługi kart i krypto, rozliczenie w USDC na wallecie, który kontrolujesz. Jeden POST tworzy checkout. Jeden webhook mówi, że zapłacono. Wysłane pod 30 minut.
Od czego zacząć
The Desk obsługuje cztery tryby integracji. Wybierz ten, który pasuje do twoich ograniczeń; API pod spodem jest to samo.
Drop-in przycisk, bez rejestracji. Podaj adres walletu USDC w body requestu. Zero stanu backendu; publiczne, widoczne w DevTools.
Server-to-server. Ukryj wallet za Bearer sk_live_. Konfiguruj branding, webhooki, mass payouty z dashboardu.
Gotowy plugin. Wgraj ZIP, wklej swój API key + webhook secret, zamówienia auto-kompletują się na płatność. HPOS-ready, WC 7.0+.
Custom App + Ręczna Metoda Płatności. ~30 min instalacji na istniejącym sklepie. Oznaczamy zamówienia jako opłacone przez Shopify Admin API.
Quickstart (5 min)
Trzy kroki: stwórz sesję, przekieruj klienta, obsłuż webhook. Sample poniżej to produkcyjny checkout route w Node.js.
// 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);To cały happy path. Klient ląduje na hosted checkoucie na , wybiera kartę albo krypto, a twój webhook odpala się w ciągu 30s od płatności.
Autentykacja
Dwa schematy, zależnie od trybu integracji:
| Schemat | Jak | Kiedy |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Po stronie serwera. Trzyma wallet prywatny. |
| Wallet w body | { "wallet": "0x…", … } | Strony statyczne / widgety bez backendu. |
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.Referencja API
Base URL: . Wszystkie endpointy gadają JSON-em, zwracają jeden obiekt na sukces, a obiekt na 4xx/5xx.
#Stwórz sesję checkoutu
Generuje URL hosted checkoutu. Klient go otwiera, płaci kartą albo krypto, The Desk rozlicza na twój wallet w USDC, webhook się odpala.
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
| amount_cents | integer | wymagane | Kwota w najmniejszej jednostce waluty (centy). Zakres 100 – 10 000 000. |
| currency | string | wymagane | Kod ISO 4217. Wspierane: EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | one-of | Wallet USDC na Polygonie (0x + 40 hex). Wymagany chyba że autoryzowany przez Bearer key. |
| customer_email | string | opcjonalne | Pokazywany w UI checkoutu i przekazywany do on-rampy dla reużycia KYC. |
| success_url | url | opcjonalne | Redirect po udanej płatności. Tylko http/https. |
| cancel_url | url | opcjonalne | Redirect jak klient porzuci checkout. |
| webhook_url | url | opcjonalne | POST target dla eventów order.paid. Nadpisuje default z dashboardu. |
| provider | string | opcjonalne | Default 'gateway' (smart picker — rekomendowany). Albo przypnij konkretny on-ramp id z GET /providers (np. moonpay, revolut, banxa, transak). |
| product_name | string | opcjonalne | Etykieta pokazywana na stronie checkoutu (max 80 znaków). |
| metadata | object | opcjonalne | Do 10 par string key/value, echo w webhooku. Zarezerwowany klucz: order_id. |
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
| id | string | opcjonalne | ID sesji, zaczyna się od cs_. |
| url | string | opcjonalne | URL hosted checkoutu, gdzie przekierować klienta. |
| status | string | opcjonalne | Zawsze "pending" przy tworzeniu. |
| amount | integer | opcjonalne | Echo amount_cents. |
| currency | string | opcjonalne | Echo currency. |
| provider | string | opcjonalne | Echo provider (default 'gateway'). |
| expires_at | string | opcjonalne | ISO 8601 wygaśnięcia (24h od utworzenia). |
| tracking_number | string | opcjonalne | Adres rozliczenia Polygon — pasuje do address_in w payloadzie webhooka, używalne z /track dla live monitoringu. |
Przykłady
// 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);#Pobierz sesję (polling)
Używaj jako fallback dla webhooka albo do hydratacji strony sukcesu po redirectcie. Server-side re-checkuje naszą warstwę rozliczenia na każdym callu — tanio (<200ms), więc polling co 3-5s jest ok.
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
| id | string | opcjonalne | ID sesji. |
| status | string | opcjonalne | pending | paid | expired | failed. |
| amount | integer | opcjonalne | Oryginalna kwota w centach. |
| currency | string | opcjonalne | Oryginalna waluta. |
| paid_at | string|null | opcjonalne | ISO 8601 kiedy zakończyło się rozliczenie on-chain. |
| paid_provider | string|null | opcjonalne | Który provider faktycznie przetworzył płatność (może się różnić od wybranego). |
| txid | string|null | opcjonalne | Txid rozliczenia Polygon. Link z polygonscan.com/tx/{txid}. |
| expires_at | string | opcjonalne | ISO 8601 wygaśnięcia. |
// 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 matryca providerów
Listuje on-rampy, które aktualnie przyjmują ruch, z minimalnymi kwotami per provider. Cache'owane 5 min na edge — pooluj raz przy bootcie appa, nie per request.
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
| providers[].id | string | opcjonalne | Klucz providera (przekazywalny jako `provider` w /checkout/init). |
| providers[].provider_name | string | opcjonalne | Human label do dropdowna. |
| providers[].status | string | opcjonalne | 'active' (zawsze filtrowane do active na tym endpoincie). |
| providers[].minimum_currency | string | opcjonalne | Kod ISO minimum. |
| providers[].minimum_amount | number | opcjonalne | Najniższa kwota, którą provider przyjmuje (w jednostkach minimum_currency). |
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.Webhooki
Gdy sesja dociera do stanu terminalnego, POST-ujemy podpisany event JSON na , który skonfigurowałeś (per-sesja albo w dashboardzie). Zawsze parsuj body requestu do weryfikacji podpisu — re-serializacja JSON zmienia kolejność kluczy i psuje 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
}Typy eventów
| Event | Kiedy |
|---|---|
| order.paid | Rozliczenie on-chain potwierdzone. + + gwarantowane obecne. Oznacz zamówienie jako opłacone. |
Tylko jest dostarczane dziś — sesje expired i failed są obserwowalne przez (status idzie na po TTL 24h; terminalne failure pokazują ). Możemy dodać push eventy dla nich w przyszłym release.
Weryfikacja podpisu
Merchanci z kontem signup dostają sekret , a każde dostarczenie niesie header w formacie . Oblicz i constant-time porównaj z . Odrzuć cokolwiek starszego niż 5 minut.
// 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');
}Polityka retry
Retry-ujemy nie-2xx odpowiedzi (i timeouty > 5s) na exponential backoff. Sześć prób w sumie przez ~42 godziny:
- Próba 1 — natychmiast przy potwierdzeniu.
- Próba 2 — +5 minut.
- Próba 3 — +15 minut.
- Próba 4 — +1 godzina.
- Próba 5 — +4 godziny.
- Próba 6 — +12 godzin, potem +24 godziny (ostatnia).
Po 6 nieudanych próbach event idzie do dead-letter. Zawsze możesz re-requestować aktualny stan przez .
Typowe problemy
- Widzę 'invalid signature' na każdym dostarczeniu
- Twój framework sparsował body jako JSON zanim go zhaszowałeś. Czytaj RAW bajty (Express: express.raw({type:'*/*'}); Next.js: req.text(); Laravel: request()->getContent(); Rails: request.raw_post). Nigdy nie re-serializuj przed haszowaniem.
- IP whitelist — z jakich IP wysyłacie?
- Dostarczenia aktualnie lecą z Vercel Edge (dynamiczne IP). Nie publikujemy statycznego zakresu. Jak MUSISZ whitelistować, użyj header podpisu jako auth gate i akceptuj dowolne source IP — HMAC to prawdziwy check tożsamości.
- HTTPS wymagane?
- Tak. Odmawiamy POST-owania na endpointy http:// (confusable deputy / plaintext replay risk). Darmowy URL https ngrok działa ok do lokalnego testowania.
- Mój endpoint jest wolny — mogę wydłużyć 5s timeout?
- Nie. Odpowiedz 2xx od razu, potem przetwarzaj asynchronicznie (job queue, setImmediate, goroutine). Długie blokujące handlery zawsze kończą się timeoutem.
SDK
API jest na tyle małe, że jest zupełnie ok — ale Node SDK daje ci typy, automatyczne retry i helper , który ogarnia weryfikację podpisu za ciebie.
// 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 DeskPełne typy, helper do webhooków, automatyczne retry.
dowolny językJeden POST, jeden GET. Bez biblioteki.
Opłaty
Flat — pełna prowizja The Desk. Bez subskrypcji, bez miesięcznej, bez opłat za chargebacki. Opłaty on-rampy kartowej (~4.5% pobierane przez upstreamowy card processor) są pass-through — klient je płaci, nigdy nie dotykają twojego payoutu.
| Metoda płatności | Ty płacisz | Klient płaci |
|---|---|---|
| Karta / Apple Pay / Google Pay | 3% | ~4.5% (on-ramp, pass-through) |
| Krypto direct (USDC → USDC) | 3% | tylko gas (~$0.01 na Polygonie) |
Pełne rozbicie z policzonymi przykładami na /fees.
Testowanie
The Desk nie ma sieci sandbox — każde rozliczenie to prawdziwy transfer on-chain. Najmniej ryzykowny test to , którą sam sobie zwracasz. Żeby przećwiczyć webhook end-to-end bez płacenia, użyj symulatora testowego poniżej.
- Symulator webhooków: POST na /api/v1/test/fire-webhook z kluczem sk_test_ odpala podpisany order.paid na twoim endpoincie — bez płatności, bez zwrotu.
- Karta dev MoonPay: , dowolna data ważności w przyszłości, dowolny CVV, ZIP 10001.
- Testowanie webhooków lokalnie: wystaw localhost przez ngrok, wklej URL do pola per sesja.
Pełna lokalna pętla (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.Błędy i rate limity
Wszystkie błędy dzielą kształt . Kody statusu są standardowe REST.
400401403404429502Troubleshooting
- Sesja jest 'paid' w dashboardzie, ale mój webhook nigdy się nie odpalił
- Sprawdź, że webhook_url jest osiągalny przez publiczny HTTPS (curl-nij go spoza twojego LAN). Jak jest ok, pooluj GET /sessions/{id} dla potwierdzenia statusu — dashboard /app pokazuje staty dostarczenia webhooka (success rate, liczniki). Sześć prób przez 42h przed dead-letter; zawsze możesz re-syncować przez polling.
- HMAC mismatch — podpis zawsze nieprawidłowy
- 99% czasu: haszujesz re-serializowane body zamiast raw bajtów. Frameworki auto-parsują JSON zanim twój handler się odpali; potrzebujesz raw buffera. Next.js: req.text() przed jakimkolwiek .json(). Express: app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails: request.raw_post. Sprawdź też, że liczysz `HMAC(whsec_secret, t + '.' + rawBody)` — NIE tylko `HMAC(whsec_secret, rawBody)`. Prefiks timestampa jest WYMAGANY.
- MoonPay mówi 'service unavailable in your country'
- MoonPay restrykcje ~20 krajów (Iran, Korea Północna, Kuba, pełna lista na ich stronie). Domyślny provider to 'gateway' — smart picker auto-fallbackuje do Revolut, Transak albo Banxa, które pokrywają inne geografie. Jak przypiąłeś konkretnego providera z provider: 'moonpay', odepnij i daj routerowi wybrać.
- Mój wallet nie dostał USDC po evencie 'paid'
- Sprawdź polygonscan.com/address/<twój-wallet> pod transfery USDC (Polygon POS). Rozliczenie ląduje 97% do ciebie i 3% do The Desk - jak nie widzisz przychodzących 97%, mogłeś wkleić zły wallet do wywołania init. Potwierdź ponownie przez GET /sessions/{id} - pole txid wskazuje prawdziwy on-chain transfer.
- Klient został obciążony dwa razy
- Nie powinno się zdarzyć. Każda sesja ma jeden addressIn rozliczenia; drugie przelew na ten sam adres staje się osobną sesją po naszej stronie i kredytujemy tylko pierwszy do twojego zamówienia. Jak się zdarzy, screenshot dwóch txid z polygonscan + id sesji i mail na hi@pay.qistdigital.com - zwrócimy duplikat z naszej treasury.
- Dostaję 502 'Payment infrastructure temporarily unavailable'
- Nasz upstream rozliczenia jest zdegradowany (< 0.5% requestów). Retry za 30s z tym samym Idempotency-Key - nasz cache zwraca oryginalną odpowiedź gdy tylko wallet się wygeneruje poprawnie. Trackuj /status dla live incydentów.
Gotowy na integrację?
Większość merchantów idzie od zera do pierwszej opłaconej transakcji pod 30 minut.