// tutorial

Guides / Manual Mode

Accept QR & bank payments with Manual Mode

Customers pay you directly. You confirm in the dashboard. Fulfill from webhooks (or verify) — don’t rely on the browser redirect alone.

What Manual Mode is

Manual Mode is for payments where money moves outside a PSP API — bank transfer, QR, or a personal wallet. SkyPay shows your account details at checkout, the customer pays you, then claims “I’ve paid.” You review and mark the payment complete.

Best for freelancers, social sellers, students, and early launches before merchant API keys.

How the flow works

Manual payments are asynchronous. Confirming a transfer can take minutes or longer — design fulfillment around that delay.

Manual payment lifecycle
  1. 1

    Customer opens checkout

    Status: pending

  2. 2

    Pays via QR or bank transfer

    Money goes to your account

  3. 3

    Taps “I’ve made the payment”

    Status: waiting

  4. 4

    You confirm in the dashboard

    Status: complete

  5. 5

    Your server fulfills the order

    Via webhook or verify API

Confirm can take minutes or hours — design for that delay.

Status path

StatusWho sets itWhat it means
pendingSkyPayCheckout open; customer hasn’t finished.
waitingCustomerThey tapped “I’ve made the payment.” Awaiting your review.
completeYouYou confirmed funds. Safe to fulfill after verify/webhook.
cancelled / invalidYou or customerRejected or abandoned — do not fulfill.

Set up Manual Mode

  1. 1 Sign up and open the merchant dashboard
  2. 2 Go to Payment Providers and add a Manual provider (bank / QR / eSewa personal, etc.)
  3. 3 Enter the account details or QR image customers should pay to
  4. 4 Choose After mark paid: wait (default) stays on verifying until Complete; continue auto-redirects to success_url after Mark as Paid
  5. 5 Generate a checkout / payment link as usual (same ck token flow)
  6. 6 Add a webhook endpoint and enable payment.waiting + payment.completed

Open providers in the dashboard →

Send customers to checkout

Same payment link pattern as the main guide. Generate a signed ck token from the dashboard, include amount, unique code, and return URLs.

html
<a href="https://app.skypay.dev/checkout?ck=YOUR_CHECKOUT_TOKEN&amount=500&code=ORDER_8821&success_url=https://yourapp.com/orders/ORDER_8821/paid&failure_url=https://yourapp.com/orders/ORDER_8821/failed">
  Pay with SkyPay
</a>

Confirm waiting payments

When a customer marks paid, the payment appears under waiting on your dashboard.

  1. 1 Open Waiting payments on your dashboard
  2. 2 Match the amount / reference / screenshot against your bank or wallet
  3. 3 Mark Complete, Cancelled, or Invalid
  4. 4 On Complete, SkyPay sets status to complete and fires payment.completed
What happens when you mark Complete

You

Dashboard → Complete

Payment moves from waiting → complete

Webhook: payment.completed

SkyPay POSTs to your HTTPS endpoint. Use this to fulfill — even if the customer left.

Redirect to success_url

Only if checkout is still open and polling. Best-effort UX — not fulfillment.

Redirects vs webhooks

This is the part that trips people up. success_url is a browser return URL. SkyPay does not server-call it when you click Complete.

Redirect vs webhook

Browser redirect

success_url / failure_url

  • For the customer’s browser
  • Needs checkout/SDK still open
  • Can be skipped if they close the tab
  • Never trust alone for fulfillment

Server webhook

payment.completed

  • Hits your backend over HTTPS
  • Fires when you confirm — tab or not
  • Signed with SkyPay-Signature
  • Then verify → fulfill the order
CustomerSkyPayYou (merchant)Your serverI've paid → waitingCompletepayment.completed webhookredirect? only if still open

If checkout is still open

Polling sees complete and redirects the customer to success_url.

If they already left (common)

No redirect. Your backend still gets payment.completed — fulfill from that.

Fulfill with webhooks

Configure an HTTPS endpoint in the dashboard (Webhooks). Enable at least payment.waiting and payment.completed.

EventWhen it firesWhat to do
payment.waitingCustomer marks paidAlert ops / start review — don’t fulfill yet
payment.completedYou mark CompleteVerify, then fulfill
payment.failed / cancelledInvalid or cancelledRelease hold / notify customer

Example handler

javascript
// Express example — POST /webhooks/skypay
app.post('/webhooks/skypay', express.raw({ type: 'application/json' }), (req, res) => {
  const body = req.body.toString('utf8');
  // Verify SkyPay-Signature: t=...,v1=hmac_sha256(secret, `${t}.${body}`)
  const event = JSON.parse(body);

  if (event.type === 'payment.completed') {
    const payment = event.data.object; // { code, status, amount, ... }
    // Prefer: re-verify, then fulfill
    // GET https://app.skypay.dev/api/v1/checkout/payments-verify/${payment.code}
  }

  res.status(200).send('ok'); // respond fast (< ~5s)
});
javascript
if (event.type === 'payment.waiting') {
  // Optional: notify ops ("new bank transfer claim")
  // Do NOT fulfill yet
}

Signatures

Each delivery includes SkyPay-Signature: t=timestamp,v1=hmac_sha256(secret, "{t}.{body}"). Respond with 2xx quickly; failed deliveries retry with backoff.

Always verify

After a webhook (or after a redirect), call verify with your API key before fulfillment. Same endpoint for every rail — including Manual.

javascript
const res = await fetch(
  'https://app.skypay.dev/api/v1/checkout/payments-verify/ORDER_8821',
  { headers: { Key: 'YOUR_API_KEY' } }
);
const { data } = await res.json();

if (data?.status === 'complete') {
  // Safe to fulfill / unlock access
}
ElementValue
MethodGET
Endpointhttps://app.skypay.dev/api/v1/checkout/payments-verify/{code}
HeaderKey: YOUR_API_KEY

Production checklist

  • Manual provider configured with correct account / QR
  • Unique code per order
  • Webhook endpoint live for payment.completed (and preferably waiting)
  • Signature verification on your server
  • Fulfill only when verify returns complete
  • Ops process to clear waiting payments promptly
  • Customer copy: “Stay on this page for instant confirmation, or we’ll email / unlock after we verify your transfer.”

FAQ

Why didn’t my customer get redirected after I confirmed?

Redirects only happen if checkout (or the Flutter SDK) is still open and polling. Manual confirm often happens later — use webhooks or payments-verify to fulfill.

Should I fulfill when status is waiting?

No. waiting means the customer claimed they paid. Fulfill only after you confirm (complete) and your server sees payment.completed or verify returns complete.

Do I need webhooks for Manual Mode?

Strongly recommended. Without them you must poll payments-verify yourself. Webhooks notify you the moment you (or your ops team) mark the payment complete.

Can customers leave before I confirm the transfer?

Yes. Set After mark paid to continue on your Manual provider: after the confirm dialog and Mark as Paid succeeds, checkout auto-redirects to success_url (status still waiting). Fulfill from payment.completed (+ verify) — see the after-mark-paid guide.

Skip the verifying wait when ops is slow

Use Continue after mark paid for immediate success_url redirect — fulfill only on payment.completed.

After mark paid settings →

Next steps

Wire webhooks, then try a test Manual payment end-to-end.