qershi sandbox

Test Qershi end to end — no real money moves.

Create a payment, mount the hosted checkout, and complete it with a test card. Everything here runs in test mode against Qershi's own router. Same API shape as production, so the integration you build now is the one you ship.

🔒 Test mode only — live payments require NBE licensing and a signed merchant agreement.

One-click test payment

Creates a real payment intent on the router, then opens the hosted checkout.

Same checkout the landing-page demo uses — this page just exposes the raw API and test cards.

Use 4242 4242 4242 4242, any future expiry, any CVC.

Test credentials

Publishable keys are safe in the browser. Secret keys never leave our server.

Publishable key
loading…
API base
loading…
Checkout SDK
loading…
Create intent
POST /api/checkout-init
The secret key (sk_…) must stay server-side. Call /api/checkout-init from your backend and pass only the client_secret to the browser.

Test cards

Every card below is handled by the Qershi test connector. Click any number to copy it, then paste it into the checkout. Expiry: any future date. CVC: any 3 digits.

Card numberResultWhat it simulates
succeedsPayment completes and is captured.
succeedsVisa-style success.
succeedsMastercard-style success.
succeedsAmex-style success.
declinedCard declined.
declinedCard declined.
declinedLost card.
declinedStolen card.
3-D SecureAuthenticates, then succeeds.

Any other number is rejected as unsupported.

Copy-paste integration

1 · Create the payment (server)

Your backend calls Qershi with the secret key. Returns a client secret.

curl -sS -X POST https://qershi.com.et/api/checkout-init \
  -H 'Content-Type: application/json' \
  -d '{"amount":2500,"currency":"ETB"}'

2 · Mount the checkout (browser)

Load the SDK, mount the payment element, then submit with confirmPayment().

const cfg = await (await fetch('/api/checkout-init', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 2500, currency: 'ETB' })
})).json()

await new Promise((ok, no) => {
  const s = document.createElement('script')
  s.src = cfg.sdk_url; s.onload = ok; s.onerror = no
  document.head.appendChild(s)
})

const hyper = window.Hyper(cfg.publishable_key,
  { customBackendUrl: cfg.backend_url })

const elements = hyper.elements({ clientSecret: cfg.client_secret })
elements.create('payment', { layout: 'tabs' }).mount('#checkout')

// The payment element renders the form but has NO submit button --
// submitting is the merchant's job. Render your own and call confirmPayment().
document.querySelector('#pay-now').addEventListener('click', async () => {
  const res = await hyper.confirmPayment({
    elements,
    confirmParams: { return_url: location.href },
    redirect: 'if_required'
  })
  if (res.status === 'succeeded') location.replace('/thanks')
})
Two gotchas we hit so you don't have to. mount() expects a CSS selector string such as '#checkout' — passing a DOM element throws selector.split is not a function. And the payment element has no built-in submit button: render your own and call hyper.confirmPayment({ elements }), which drives POST /payments/<id>/confirm on the router.
Qershi Checkout sandbox