Cashirly Merchant API
Terima QRIS, VA, dan metode lain lewat satu API white-label. Kredensial TEST dan LIVE terpisah; checkout di-host di domain Cashirly; status final lewat GET payment + webhook bertanda tangan.
TEST / sandbox
Kunci sk_test_. MockProvider — tidak ada uang sungguhan. Simulasi di /pay/sandbox/…
LIVE
Kunci sk_live_. Butuh KYC APPROVED + liveEnabled. Uang sungguhan lewat rail PSP.
Secret key hanya di server Anda. Front-end hanya redirect ke checkout_url yang dikembalikan API.
Autentikasi
Dua mode autentikasi — jangan dicampur di satu request.
| Kredensial | Header | Pakai untuk |
|---|---|---|
sk_test_… / sk_live_… | Authorization: Bearer atau X-Api-Key | /payments, /refunds, /payment-methods |
cs_… (session) | Authorization: Bearer | Dashboard: api-keys, webhooks, links, invoices, KYC |
Tidak ada pk_test_/pk_live_ publishable key. LIVE secret ditolak jika merchant belum APPROVED atau liveEnabled=false.
Authorization: Bearer sk_test_… Cashirly-Version: 2026-08-02 Idempotency-Key: <unique-per-create> Content-Type: application/json
Quick start
- Buat kunci TEST di /dashboard/api-keys
- Pasang webhook HTTPS + simpan whsec_ (ditampilkan sekali). /dashboard/api-keys
- POST /v1/payments dengan sk_test_ → ambil checkout_url → redirect pelanggan.
- Di sandbox hop, Simulate paid/failed → terima webhook → GET payment untuk konfirmasi.
- Setelah KYC disetujui admin, buat sk_live_ dan ulangi alur dengan uang kecil sungguhan.
curl -s -X POST "https://api.cashirly.com/v1/payments" \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-$(date +%s)" \
-H "Cashirly-Version: 2026-08-02" \
-d '{
"amount": "10000",
"currency": "IDR",
"description": "Demo order",
"reference_id": "order-1001",
"payment_method": "QRIS",
"customer_name": "Budi Santoso",
"customer_email": "[email protected]",
"customer_phone": "081234567890",
"success_url": "https://yoursite.com/pay/ok",
"cancel_url": "https://yoursite.com/pay/cancel"
}'Setup merchant
Satu integrasi kode; lingkungan dikontrol oleh prefix kunci dan gerbang KYC di dashboard.
- TEST — selalu MockProvider; ledger TEST terpisah.
- LIVE — verificationStatus=APPROVED dan liveEnabled=true (setelah admin approve KYC).
- Nama rail PSP tidak pernah dikembalikan ke merchant response (white-label).
POST /v1/payments
amount wajib string digit (minor units). Wajib Idempotency-Key di production.
| Field | Wajib | Catatan |
|---|---|---|
amount | yes | String, mis. "10000" untuk Rp10.000 |
currency | no | Default IDR |
reference_id | no | ID order Anda (bukan order_id) |
payment_method | no | QRIS, VA, … dari discovery |
customer_name / customer_email / customer_phone | no | Tampil di halaman /pay — kirim dari toko Anda (tidak di-scrape) |
success_url / cancel_url | no | HTTPS redirect setelah hop |
curl -s -X POST "https://api.cashirly.com/v1/payments" \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-$(date +%s)" \
-H "Cashirly-Version: 2026-08-02" \
-d '{
"amount": "10000",
"currency": "IDR",
"description": "Demo order",
"reference_id": "order-1001",
"payment_method": "QRIS",
"customer_name": "Budi Santoso",
"customer_email": "[email protected]",
"customer_phone": "081234567890",
"success_url": "https://yoursite.com/pay/ok",
"cancel_url": "https://yoursite.com/pay/cancel"
}'Response berisi id, status, checkout_url. Redirect pelanggan hanya ke checkout_url dari Cashirly — jangan buat URL PSP sendiri (cegah middleman hijack).
Pakai Idempotency-Key dan reference_id unik tiap order. Create LIVE bisa ~45 detik (rail). Jika gagal: pesan white-label (tanpa nama PSP) — coba key baru. Pastikan KYC APPROVED + admin Providers siap.
GET / cancel
curl -s "https://api.cashirly.com/v1/payments/pay_..." \ -H "Authorization: Bearer sk_test_YOUR_KEY" \ -H "Cashirly-Version: 2026-08-02"
curl -s -X POST "https://api.cashirly.com/v1/payments/pay_.../cancel" \ -H "Authorization: Bearer sk_test_YOUR_KEY" \ -H "Cashirly-Version: 2026-08-02"
Jangan anggap order lunas hanya karena pelanggan kembali ke success_url atau halaman PSP menampilkan paid. Mark order paid di toko Anda hanya setelah:
- Webhook bertanda tangan
payment.paid, atau GET /v1/payments/:idmengembalikanstatus: "paid"
Admin dashboard, merchant dashboard, dan toko membaca status yang sama dari Cashirly. Webhook data: id (Cashirly), reference_id (toko), order_id (rail). Endpoint webhook harus balas HTTP 2xx setelah verifikasi signature — 401/404 = toko tidak pernah update (cek DLQ di dashboard).
Refund
POST /v1/refunds dengan sk_*. Banyak rail LIVE mengembalikan MANUAL_REQUIRED — admin menyelesaikan di Transactions.
curl -s -X POST "https://api.cashirly.com/v1/refunds" \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Cashirly-Version: 2026-08-02" \
-d '{"payment_id":"pay_..."}'Metode bayar
curl -s "https://api.cashirly.com/v1/payment-methods" \ -H "Authorization: Bearer sk_test_YOUR_KEY" \ -H "Cashirly-Version: 2026-08-02"
Environment mengikuti kunci (TEST vs LIVE). Response hanya id metode Cashirly — tanpa nama PSP.
Webhook
HMAC-SHA256 hex atas raw body JSON. Header: X-Cashirly-Signature, X-Cashirly-Timestamp. Saat rotasi: X-Cashirly-Signature-Next. Panduan lengkap: docs/integrations/merchant-webhooks.md
| Jenis | Daftar di | HTTP | Contoh |
|---|---|---|---|
| Webhook (server) | Dashboard → endpoint webhook | POST | …/notify atau …/hooks/cashirly |
| Return (browser) | success_url / cancel_url saat create payment | GET | …/success — bukan webhook |
Daftarkan /success sebagai webhook → DLQ HTTP 405. Duplikat URL notify (dua secret) → mudah HTTP 401. Satu endpoint notify + satu whsec_ saja.
| Event | Kapan |
|---|---|
payment.paid | Pembayaran lunas di Cashirly (setelah rail callback/sync atau sandbox simulate). Inilah sinyal untuk update stok/order di toko. |
payment.failed | Gagal / sandbox simulate failed |
payment.cancelled | Dibatalkan |
payment.refunded | Refund selesai |
{
"id": "evt_…",
"type": "payment.paid",
"created_at": "2026-08-10T05:00:00.000Z",
"data": {
"id": "cms…",
"status": "paid",
"amount": "10000",
"currency": "IDR",
"reference_id": "cr-shop-OTAA8TNFSS1L",
"order_id": "ba5bfe46-b810-4958-86c6-1365dac3bbd0",
"paid_at": "2026-08-10T05:01:00.000Z"
}
}const crypto = require('crypto');
function verifyCashirlyWebhook(rawBody, signatureHeader, whsec) {
const expected = crypto
.createHmac('sha256', whsec) // full whsec_... string
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signatureHeader || '', 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Register ONLY a server route (e.g. /notify or /hooks/cashirly).
// Do NOT register success_url pages (/success) as webhooks → HTTP 405 in DLQ.
// Express: express.raw({ type: 'application/json' }) for this route only.
app.post('/membership/cashirly/notify', (req, res) => {
const ok = verifyCashirlyWebhook(
req.body, // Buffer / raw string — not JSON.parse first
req.get('X-Cashirly-Signature'),
process.env.CASHIRLY_WEBHOOK_SECRET,
);
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// event.type: payment.paid | payment.failed | payment.cancelled | payment.refunded
// Match shop order: event.data.reference_id (e.g. cr-shop-...) or event.data.id
// event.data.order_id = rail Order ID (Cashlez UUID) — optional
if (event.type === 'payment.paid') {
// markOrderPaid(event.data.reference_id)
}
res.status(200).json({ received: true });
});| Error DLQ | Arti | Perbaiki di |
|---|---|---|
HTTP 405 | URL hanya terima GET (halaman /success). Cashirly kirim POST. | Toko + hapus webhook /success di dashboard |
HTTP 401 | Signature gagal, whsec_ salah/duplikat, atau middleware auth sebelum verify. | Toko (secret + raw body) + satu endpoint saja |
HTTP 404 | Route POST tidak ada. | Toko |
- Verifikasi signature dengan timing-safe compare (HMAC atas raw body + full whsec_).
- Pakai raw body — jangan JSON.stringify ulang objek parsed.
- Jangan gate login/session pada URL webhook.
- Setelah endpoint benar: Replay DLQ di dashboard untuk event tertinggal.
API sandbox
Endpoint publik hanya untuk pembayaran environment=TEST. ID LIVE selalu 404 (tidak bocor ke hop sandbox).
GET https://api.cashirly.com/v1/sandbox/payments/:idPOST https://api.cashirly.com/v1/sandbox/payments/:id/simulatebody{"outcome":"paid"|"failed"}
curl -s -X POST "https://api.cashirly.com/v1/sandbox/payments/pay_.../simulate" \
-H "Content-Type: application/json" \
-d '{"outcome":"paid"}'
# outcome: "paid" | "failed" — TEST payments onlyUI hop: buka checkout_url → tombol Simulate paid / Simulate failed. Webhook ikut terkirim jika endpoint aktif.
API sesi
Memakai Bearer cs_… (login dashboard). Bukan sk_.
/v1/merchants/me/api-keys/v1/merchants/me/webhooks(+ rotate, DLQ)/v1/merchants/me/payment-links/v1/merchants/me/invoices/v1/merchants/me/api-logs/v1/merchants/kyc/submit
OpenAPI interaktif di host API: /docs (bukan halaman marketing /docs).
Panduan testing
- Login merchant → buat sk_test_ → salin sekali.
- Daftarkan webhook HTTPS uji (ngrok/localtunnel OK untuk staging).
- POST payment amount "10000" → buka checkout_url.
- Simulate paid → cek webhook payment.paid + GET status paid.
- Buat payment baru → Simulate failed → cek payment.failed.
- Uji refund TEST bila status paid (atau MANUAL_REQUIRED di LIVE).
Secret tetap di server/CI Anda. Docs ini + dashboard + sandbox hop sudah cukup untuk uji end-to-end.
Checklist go-live
- Selesaikan KYC di /dashboard/verification.
- Admin approve → liveEnabled aktif.
- Buat sk_live_ (jangan commit ke git).
- Webhook production HTTPS + verifikasi signature (raw body).
- Transaksi kecil sungguhan → pastikan webhook payment.paid sampai + GET status paid (bukan hanya success_url).
- Toko: kirim customer_name / customer_email / customer_phone pada create; mark paid hanya dari webhook/GET.
- Siapkan runbook refund / dispute.
Keamanan
- PCI scope: Cashirly tidak menerima PAN/CVV di API merchant. Jangan kirim data kartu.
- Middleman: Hanya redirect ke checkout_url dari response create. Jangan hardcode domain PSP.
- Idempotency: Satu kunci unik per create payment — mencegah double charge saat retry.
- Webhook: HMAC + raw body + HTTPS. Jangan percaya IP allowlist saja.
- Secrets: sk_* dan whsec_* hanya di server/secret store. Rotasi webhook dari dashboard.
- Sandbox: Simulate tidak pernah menyentuh LIVE. Lookup LIVE via /sandbox → 404. Rate limit ketat di path sandbox.
- Webhook URL: Endpoint baru harus HTTPS. Cashirly memblokir target IP privat/metadata saat kirim. Rotasi secret: dual HMAC — lihat docs keamanan WEBHOOK_SECRET_ROTATION.
- Bukan klaim 100%: Kontrol ini mengurangi risiko integrasi umum. Laporkan temuan via /legal/report-bug — jangan klaim PCI L1 tanpa QSA.
Error
| HTTP | Arti umum |
|---|---|
| 401 | Kunci hilang/salah atau session expired |
| 400 | Body invalid, amount bukan string digit, atau LIVE gate (belum APPROVED/liveEnabled) |
| 404 | Payment/sandbox id tidak ada (atau LIVE di path sandbox) |
| 409 | Konflik idempotency / state |
Dukungan
Buka dashboard untuk kunci, webhook, dan log API. OpenAPI Nest: host API + /docs.