Migrating a Payze integration to QuickPay
A developer's guide to moving an existing integration. If you're still deciding whether QuickPay is the right replacement, start with the overview.
Most migrations are one to two days of work. Three things move; everything else in your application stays where it is.
Before you start
You need an active merchant account with at least one Georgian bank, a QuickPay account with those credentials entered, and an API key.
Keys are brand-scoped and come in two forms — qpk_test_... for sandbox and qpk_live_... for production. Test keys only work while the brand is in test mode, so a test run can't accidentally charge a real card.
1. Charge creation → POST /v1/payments
Wherever your code creates a charge, it now posts to https://api.quickpay.ge/v1/payments with a Bearer token. The response contains a payment_url — redirect the customer there.
POST https://api.quickpay.ge/v1/payments
Authorization: Bearer qpk_live_...
Idempotency-Key: order-10432
Content-Type: application/json
{
"amount": 149.99,
"currency": "GEL",
"gateway_slug": "bog_card",
"merchant_order_id": "ORDER-10432",
"description": "Order #10432",
"customer_name": "Nini Beridze",
"customer_email": "nini@example.ge",
"customer_phone": "+995555123456",
"return_url": "https://yourstore.ge/thank-you",
"cancel_url": "https://yourstore.ge/cart",
"webhook_url": "https://yourstore.ge/webhooks/quickpay"
}
Only amount is required. Everything else is optional and shown because most real integrations need it: return_url and cancel_url are where the customer lands after paying or cancelling, webhook_url overrides your brand-level endpoint for this one payment, merchant_order_id carries your own order reference, and the customer_* fields prefill checkout.
gateway_slug is optional. Omit it and the customer picks their own method on QuickPay's hosted checkout — card, installment, BNPL, crypto, bank transfer. If you'd built your own payment method selector, you can delete it.
Amounts are decimals, not minor units. 149.99, not 14999. This catches nearly every migration at least once. GEL is the default; USD, EUR and GBP are supported.
The Idempotency-Key header prevents double-charges on retry. Replaying a key returns 200 with the original payment; replaying it with a different amount or currency returns 409.
2. Status polling → webhooks
If your integration polled for payment status, stop. QuickPay pushes.
Events: payment.paid, payment.failed, payment.refunded, payment.partially_refunded, payment.refund_pending, payment.cancelled, payment.expired, plus subscription.charged, subscription.failed, invoice.paid and lead.submitted.
Every request carries a signature header:
QUICKPAY-SIGNATURE: t=1754300000,v1=8f3a...
v1 is HMAC-SHA256 over the string "{timestamp}.{raw_json_body}", keyed with your webhook secret:
[$t, $v1] = parse_signature($_SERVER['HTTP_QUICKPAY_SIGNATURE']);
if (abs(time() - $t) > 300) {
return response('stale', 400); // 5-minute skew limit
}
$expected = hash_hmac('sha256', "{$t}.{$rawBody}", $webhookSecret);
if (!hash_equals($expected, $v1)) {
return response('bad signature', 400);
}
Sign against the raw request body, before any JSON parsing or re-encoding. Re-serialising the payload changes the bytes and the signature will never match. This is the single most common bug in any HMAC webhook implementation — if verification fails and everything looks correct, this is why.
Your endpoint must return 2xx within 30 seconds. Failed deliveries retry up to 5 times, so handlers must be idempotent — you will receive the same event twice eventually.
3. Refunds → POST /v1/payments/{uuid}/refund
Full or partial. Omit amount to refund the remaining balance. Returns the updated payment object.
Refunds can sit in payment.refund_pending before completing, depending on the gateway. Handle both that and payment.refunded.
Testing
Put your brand in test mode, use your <code>qpk_test_...</code> key, and run the flow end to end. No real gateway calls are made.
The dashboard includes an API Playground for firing requests without writing code — the fastest way to confirm your payload shape before touching application code.
Go-live checklist
- Bank merchant credentials entered and the module activated
- Sandbox run completed for every payment method you plan to offer
- Webhook endpoint deployed and signature verification tested against a real payload
- Handler confirmed idempotent — replay the same event twice
- Timestamp skew check in place
- Brand switched out of test mode,
qpk_test_swapped forqpk_live_ - One live transaction at minimum value, then refund it
- Old provider's webhook endpoint left running for a week to catch stragglers
Reference
- Full API documentation
- SDKs — PHP, Laravel, Node/TypeScript
- Rate limit: 100 requests/minute per API key, reported via
X-RateLimit-LimitandX-RateLimit-Remaining
Stuck on something specific? Send us the details — describe your current integration and we'll map it.