API Documentation
Base URL: https://api.quickpay.ge/v1
What you can build
Quickpay is not just a card payment API. A single POST /v1/payments call covers six distinct payment flows — the gateway_slug field determines which flow the customer sees on the hosted payment page.
payment_url → receive the payment.paid webhook. Only the gateway_slug changes.💳 Card payment
One-time card charge. Customer enters card details at the bank's secure interface. Visa/MC, Apple Pay, and Google Pay where supported.
bog_card · tbc_card · flitt · unipay · fastoo · moka · keepz📆 Installment loan
Customer picks a repayment plan (3–36 months). Merchant receives the full amount upfront — the bank handles instalment collection.
bog_installment · tbc_installment · credo_installment · flitt_installment0️⃣ BNPL (0% financing)
Buy-now-pay-later split into equal 0-interest instalments. The bank absorbs the financing cost — no extra charge to customer or merchant.
bog_bnpl · flitt_bnpl📱 Open banking / QR / Wallet
Card + QR code + open banking + digital wallet in one gateway. Supports all major Georgian banks via open banking.
keepz · paysera₿ Crypto
Customer pays in cryptocurrency. CityPay handles the conversion and settles GEL to the merchant. No crypto wallet required on the merchant side.
citypay🏦 Offline / manual
No hosted payment page. Bank transfer: customer receives your IBAN + order UUID as reference. COD: merchant confirms delivery manually.
bank_transfer · cash_on_deliveryQuick Start
Get your first real payment working in three steps.
1. Get your API key — Dashboard → API Keys → copy your qpk_live_... key. Your key is tied to one Brand in your account.
2. Create a payment — call POST /v1/payments with your amount, description, and return URL. The response contains a payment_url.
curl -X POST https://api.quickpay.ge/v1/payments \
-H "Authorization: Bearer qpk_live_..." \
-H "Idempotency-Key: order-1234" \
-H "Content-Type: application/json" \
-d '{
"amount": 149.99,
"currency": "GEL",
"description": "Order #1234",
"return_url": "https://yourshop.ge/thank-you"
}'3. Redirect & handle webhook — redirect your customer to the payment_url. When payment completes, Quickpay POSTs a payment.paid event to your Webhook URL (Dashboard → Brand Settings). See Webhooks.
qpk_test_... key — no real charges, no real gateway calls. Switch back to your live key for production.Authentication
All API requests (except exchange rates) require a bearer token. Use your live key (qpk_live_...) for production and your test key (qpk_test_...) for development. Keys are brand-scoped — every key is tied to one Brand in your account.
Authorization: Bearer qpk_live_your_key_hereRate limit: 100 requests per minute per API key. Rate-limit headers are returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining.
Idempotency
Include an Idempotency-Key header on all POST requests to safely retry without double-charging. The same key returns the same payment response (HTTP 200). If you supply a key that was previously used with a *different amount or currency*, the API returns 409 Conflict.
Idempotency-Key: order-1234-attempt-1Amounts & Currencies
All monetary values are decimal numbers. 5.00₾ = "amount": 5.00. Supported currencies: GEL · USD · EUR · GBP. When currency is omitted it defaults to GEL.
Errors
Errors always include a message field and a machine-readable error_code string. Validation errors (422) also include an errors object keyed by field name. See Error codes for the full list.
| Code | Meaning |
|---|---|
| 401 | Missing or invalid API key |
| 403 | Account suspended, test key on live brand, or no active gateway subscription |
| 404 | Resource not found |
| 409 | Idempotency key conflict (different amount or currency) |
| 422 | Validation failed — see the errors object |
| 429 | Rate limit or charge-frequency exceeded — Retry-After present |
| 500 | Server or gateway error |
Official SDKs
Don't want to call the raw REST API by hand? These are thin, typed clients over this same API — no card data ever touches your server.
| SDK | Package | Install | Repository |
|---|---|---|---|
| PHP SDK | quickpayge/php-sdk | composer require quickpayge/php-sdk | quickpay.ge/php-sdk · GitHub |
| Laravel Package | quickpayge/laravel | composer require quickpayge/laravel | quickpay.ge/laravel-package · GitHub |
| Node.js / TypeScript SDK | @quickpay/js | npm install @quickpay/js | quickpay.ge/node-sdk · GitHub |
Platform Plugins
Building on WooCommerce, WHMCS, OpenCart, CS-Cart, PrestaShop, or Easy Digital Downloads? Skip the API entirely — install the matching plugin and configure it with an API key.
| Platform | Type | Page |
|---|---|---|
| WooCommerce (WordPress) | E-commerce | quickpay.ge/wordpress-plugin |
| WHMCS | Hosting / billing | quickpay.ge/whmcs-plugin |
| OpenCart | E-commerce | quickpay.ge/opencart-plugin |
| CS-Cart | E-commerce | quickpay.ge/cscart-plugin |
| PrestaShop | E-commerce | quickpay.ge/prestashop-plugin |
| Easy Digital Downloads (WordPress) | Digital goods | quickpay.ge/edd-plugin |
Plugins are downloaded from Dashboard → Integrations after registering, and support self-hosted one-click updates.
Create a payment
POST /v1/payments → Try in Playground
Creates a payment and returns a payment_url — redirect your customer there to complete checkout. Omit gateway_slug to let the customer choose their preferred gateway on the hosted payment page.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| amount | number | required | Payment amount as a decimal (e.g. 49.99). Min 0.01, up to 2 decimal places. |
| currency | string | optional | One of GEL, USD, EUR, GBP. Defaults to GEL. |
| description | string | optional | Order description shown to the customer (max 500 chars). |
| gateway_slug | string | optional | Pre-select a gateway. Omit to show all available gateways. See List gateways and What you can build for valid slugs. |
| merchant_order_id | string | optional | Your own order reference — stored and returned on all responses (max 255 chars). |
| bank_transfer_reference | string | optional | Custom reference shown to the customer on the Bank Transfer instructions (e.g. your own invoice number). Only applies to bank_transfer payments — falls back to a Quickpay-generated code when omitted (max 140 chars). |
| customer_name | string | optional | Customer full name — pre-fills the checkout form (max 255 chars). |
| customer_email | string | optional | Customer email — pre-fills the form and used for the receipt. |
| customer_phone | string | optional | Customer phone — pre-fills the checkout form. |
| return_url | string | optional | Redirect URL after checkout completes. |
| webhook_url | string | optional | Webhook endpoint for this payment only. Overrides the brand-level webhook URL configured in the dashboard. Useful for multi-tenant setups where each payment routes to a different endpoint. |
| expires_at | datetime | optional | ISO 8601 datetime after which the payment link expires. Must be in the future. |
| coupon_code | string | optional | Coupon code to apply — the discount is deducted from amount before charging (max 50 chars). Only applied when gateway_slug is set; silently ignored on gateway-less payments. |
| product_id | integer | optional | Internal product ID to attach to the payment (used for coupon product-scoping). |
| product_image_url | string | optional | Product image URL shown on the checkout page. |
| line_items | array | optional | Itemised breakdown. The sum of line_items[].total must equal amount. |
| checkout_template_id | integer | optional | Override the checkout page template for this payment. Use an id from List checkout templates. |
| custom_fields_schema | array | optional | Extra form fields shown to the customer — see the schema below. |
| prefill_only | boolean | optional | When true, pre-fill customer info but still show the form for confirmation. |
| source | string | optional | Integration source tag: woocommerce, whmcs, opencart, cscart, prestashop, edd, or api. |
| idempotency_key | string | optional | Alternative to the Idempotency-Key header (max 255 chars). |
line_items[] object
| Parameter | Type | Description | |
|---|---|---|---|
| type | string | required | One of product, shipping, tax, fee, discount. |
| name | string | required | Line item label in English (max 255 chars). |
| name_ka | string | optional | Line item label in Georgian (max 255 chars). |
| qty | integer | required | Quantity (min 1). |
| unit_price | number | required | Price per unit. |
| total | number | required | Line total. The sum of every total must equal amount. |
| sku | string | optional | Product SKU (max 100 chars). |
custom_fields_schema[] object
| Parameter | Type | Description | |
|---|---|---|---|
| label | string | required | Field label shown to the customer (max 200 chars). |
| type | string | required | One of text, textarea, select, number, checkbox, radio. |
| required | boolean | optional | Whether the customer must fill this field. |
| options | array | optional | Required for select and radio. Each item: { "label": "…", "value": "…" }. |
Request example
curl -X POST https://api.quickpay.ge/v1/payments \
-H "Authorization: Bearer qpk_live_..." \
-H "Idempotency-Key: order-1234" \
-H "Content-Type: application/json" \
-d '{
"amount": 149.99,
"currency": "GEL",
"description": "Order #1234",
"gateway_slug": "bog_card",
"merchant_order_id": "ORD-1234",
"customer_name": "Giorgi Beridze",
"customer_email": "[email protected]",
"return_url": "https://yourshop.ge/thank-you"
}'$response = Http::withToken('qpk_live_...')
->withHeaders(['Idempotency-Key' => 'order-1234'])
->post('https://api.quickpay.ge/v1/payments', [
'amount' => 149.99,
'currency' => 'GEL',
'description' => 'Order #1234',
'gateway_slug'=> 'bog_card',
'return_url' => 'https://yourshop.ge/thank-you',
]);
$paymentUrl = $response->json('payment_url');const res = await fetch('https://api.quickpay.ge/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer qpk_live_...',
'Idempotency-Key': 'order-1234',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 149.99, currency: 'GEL', description: 'Order #1234',
gateway_slug: 'bog_card', return_url: 'https://yourshop.ge/thank-you',
}),
});
const { payment_url } = await res.json();import requests
r = requests.post(
'https://api.quickpay.ge/v1/payments',
headers={
'Authorization': 'Bearer qpk_live_...',
'Idempotency-Key': 'order-1234',
},
json={
'amount': 149.99, 'currency': 'GEL', 'description': 'Order #1234',
'gateway_slug': 'bog_card', 'return_url': 'https://yourshop.ge/thank-you',
},
)
payment_url = r.json()['payment_url']// 201 Created (200 on idempotency replay)
{
"uuid": "018f2a3b-...",
"merchant_order_id": "ORD-1234",
"payment_url": "https://qpy.ge/pay/018f2a3b-...",
"status": "pending",
"amount": 149.99,
"currency": "GEL",
"discount_gel": null,
"coupon_code": null,
"redirect_url": null
}gateway_slug is omitted the customer picks the gateway on the hosted page, and the response omits discount_gel, coupon_code, and redirect_url.Idempotency-Key returns the original payment with HTTP 200; a different amount or currency for the same key returns 409.bank_transfer_reference is stored even when gateway_slug is omitted — it takes effect once the customer selects Bank Transfer on the hosted page.Get a payment
GET /v1/payments/{uuid} → Try in Playground
Returns the full payment object.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Payment UUID. |
// 200 OK
{
"uuid": "018f2a3b-...",
"merchant_order_id": "ORD-1234",
"status": "paid",
"amount": 149.99,
"currency": "GEL",
"description": "Order #1234",
"customer_name": "Giorgi Beridze",
"customer_email": "[email protected]",
"gateway_order_id": "BOG-XYZ789",
"surcharge": null,
"coupon": null,
"form_data": {},
"line_items": null,
"refunded_amount": 0,
"refunds": [],
"paid_at": "2026-05-16T10:23:00Z",
"expires_at": null,
"created_at": "2026-05-16T10:20:00Z"
}Response fields
| Field | Type | Description |
|---|---|---|
| uuid | string | Unique payment identifier (UUID v7). |
| merchant_order_id | string|null | Your order reference passed at creation. |
| status | string | Payment status — see the list below. |
| amount | number | Charged amount. |
| currency | string | Currency code. |
| description | string|null | Order description. |
| customer_name | string|null | Customer full name. |
| customer_email | string|null | Customer email address. |
| gateway_order_id | string|null | Order ID assigned by the payment gateway. |
| bank_transfer_reference | string|null | Reference shown to the customer for Bank Transfer payments — your requested value, or a Quickpay-generated code if none was provided. |
| surcharge | number|null | Gateway surcharge applied, if any. |
| coupon | object|null | Applied coupon details (code, type, discount_gel). |
| form_data | object | Custom field values submitted by the customer. |
| paid_at | datetime|null | ISO 8601 timestamp when payment was confirmed. |
| expires_at | datetime|null | ISO 8601 expiry timestamp. |
| created_at | datetime | ISO 8601 creation timestamp. |
| line_items | array|null | Itemised breakdown, if provided at creation. |
| refunded_amount | number | Total amount refunded so far. |
| refunds | array | Refund records (uuid, amount, reason, status, initiated_by, created_at). |
pending · paid · failed · refunded · partially_refunded · cancelled · expired · transfer_pending · cod_pending · deposit_paid.List payments
GET /v1/payments → Try in Playground
Returns a paginated list (20 per page) sorted newest first.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | optional | Filter by status — see the status list above. |
| gateway | string | optional | Filter by gateway slug (e.g. bog_card, tbc_card). |
| from | date | optional | Start date — YYYY-MM-DD. |
| to | date | optional | End date — YYYY-MM-DD. Must be ≥ from. |
| page | integer | optional | Page number (default 1). |
// 200 OK
{
"data": [ { "...payment object..." } ],
"meta": { "current_page": 1, "last_page": 5, "total": 92 }
}Refund a payment
POST /v1/payments/{uuid}/refund → Try in Playground
Initiates a full or partial refund. Omit amount to refund the full remaining amount. Returns the updated payment object.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Payment UUID. |
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| amount | number | optional | Amount to refund. Defaults to the full remaining refundable amount. |
| reason | string | optional | Internal reason for the refund — stored in the audit log (max 500 chars). |
{ "amount": 50.00, "reason": "Customer requested partial refund" }Returns 200 OK.
refunds array and refunded_amount.List products
GET /v1/products → Try in Playground
Returns a paginated list (20 per page) sorted newest first.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| active | boolean | optional | true or false — filter by active status. |
| page | integer | optional | Page number (default 1). |
// 200 OK
{
"data": [
{
"uuid": "018f...",
"name_en": "Product Name",
"name_ka": "პროდუქტის სახელი",
"short_description_en": "...",
"short_description_ka": "...",
"price": 99.99,
"compare_price": 129.99,
"base_currency": "GEL",
"accepted_currencies": ["GEL", "USD"],
"sku": "SKU-001",
"track_stock": false,
"stock_quantity": null,
"image_url": "https://...",
"categories": ["clothing"],
"custom_fields_schema": [],
"is_active": true,
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:00Z"
}
],
"meta": { "current_page": 1, "last_page": 1, "total": 3 }
}Create a product
POST /v1/products → Try in Playground
At least one of name_en or name_ka is required.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| name_en | string | optional | Product name in English (required if name_ka is absent). |
| name_ka | string | optional | Product name in Georgian (required if name_en is absent). |
| short_description_en | string | optional | Short description in English. |
| short_description_ka | string | optional | Short description in Georgian. |
| price | number | required | Sale price (min 0.01). |
| compare_price | number | optional | Original/crossed-out price shown to highlight a discount. |
| base_currency | string | optional | Base pricing currency. One of GEL, USD, EUR, GBP. Defaults to GEL. |
| accepted_currencies | array | optional | Currencies the customer may pay in (e.g. ["GEL","USD"]). |
| sku | string | optional | Stock keeping unit identifier (max 100 chars). |
| track_stock | boolean | optional | Enable stock tracking. Defaults to false. |
| stock_quantity | integer | optional | Current stock level. Only used when track_stock is true. |
| categories | array | optional | Array of category strings (e.g. ["clothing","sale"]). |
| custom_fields_schema | array | optional | Extra checkout form fields — same structure as the payment custom_fields_schema. |
| is_active | boolean | optional | Whether the product is available for purchase. Defaults to true. |
Returns 201 Created.
Get a product
GET /v1/products/{uuid} → Try in Playground
Returns the full product object.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Product UUID. |
Returns 200 OK.
Response fields
| Field | Type | Description |
|---|---|---|
| uuid | string | Unique product identifier. |
| name_en | string|null | Product name in English. |
| name_ka | string|null | Product name in Georgian. |
| short_description_en | string|null | Short description in English. |
| short_description_ka | string|null | Short description in Georgian. |
| price | number | Sale price. |
| compare_price | number|null | Original/crossed-out price. |
| base_currency | string | Base pricing currency. |
| accepted_currencies | array | Currencies the customer may pay in. |
| sku | string|null | Stock keeping unit. |
| track_stock | boolean | Whether stock tracking is enabled. |
| stock_quantity | integer|null | Current stock level. |
| image_url | string|null | Primary product image URL. |
| categories | array | Category strings. |
| custom_fields_schema | array | Extra checkout fields. |
| is_active | boolean | Whether the product is available. |
| created_at | datetime | Creation timestamp. |
| updated_at | datetime | Last update timestamp. |
Update a product
PUT /v1/products/{uuid} → Try in Playground
Partial update — include only the fields you want to change. Same field rules as create. Returns the full product object.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Product UUID. |
Returns 200 OK.
Delete a product
DELETE /v1/products/{uuid} → Try in Playground
Deletes the product.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Product UUID. |
// 200 OK
{ "deleted": true }Create a checkout link
POST /v1/checkout-links → Try in Playground
Creates a reusable payment link. Provide either product_uuid (price comes from the product) or a fixed amount.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| product_uuid | string | optional | Attach an existing product — its price is used automatically. Required if amount is absent. |
| amount | number | optional | Fixed amount in the chosen currency. Required if product_uuid is absent. |
| currency_override | string | optional | Override the default currency: GEL, USD, EUR, or GBP. |
| hide_currency_selector | boolean | optional | Hide the currency selector on checkout. Defaults to false. |
| expires_at | datetime | optional | ISO 8601 datetime after which the link is no longer usable. |
| max_uses | integer | optional | Maximum number of times the link can be used (min 1). |
| allowed_gateways | array | optional | Restrict which gateway slugs are shown. Defaults to all active gateways. |
| custom_fields_schema | array | optional | Extra checkout form fields (see note below). |
| checkout_settings | object | optional | Checkout page behaviour overrides (same structure as Brand Checkout Settings). |
{
"amount": 50.00,
"currency_override": "GEL",
"expires_at": "2026-12-31T23:59:59Z",
"max_uses": 100
}// 201 Created
{
"uuid": "018f...",
"url": "https://qpy.ge/p/abc123",
"slug": "abc123",
"product_uuid": null,
"amount": 50.00,
"currency": "GEL",
"expires_at": "2026-12-31T23:59:59+00:00",
"max_uses": 100,
"is_active": true,
"created_at": "2026-06-01T09:00:00+00:00"
}custom_fields_schema uses the same shape as payments except radio is not supported and label is capped at 100 chars. select fields require an options array of { "label": "…", "value": "…" }.Embed Button
Add a Quickpay checkout button to any HTML page with one script tag. Pass a checkout link UUID in data-checkout-link; the script renders a styled <a> immediately after the script tag, so right-click/open-in-new-tab still works.
<script src="https://qpy.ge/embed.js"
data-checkout-link="YOUR_CHECKOUT_LINK_UUID"
data-label="Buy Now"
data-color="#2563eb">
</script>| Attribute | Default | Description |
|---|---|---|
data-checkout-link | — | Required. Checkout link UUID/slug or a full https:// URL. |
data-label | გადახდა | Button text. |
data-color | #2563eb | Button background color. |
data-text-color | #ffffff | Button text color. |
data-size | md | sm, md, or lg. |
data-full-width | false | Set to true for a block button that stretches to the container width. |
data-radius | 8px | CSS border-radius value. |
data-target | _self | Use _blank to open checkout in a new tab with rel="noopener noreferrer". |
script-src https://qpy.ge.List invoices
GET /v1/invoices → Try in Playground
Returns a paginated list (20 per page) sorted newest first.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | optional | One of draft, sent, viewed, paid, cancelled, overdue. |
| customer_email | string | optional | Filter by exact customer email. |
| from | date | optional | Start date — YYYY-MM-DD. |
| to | date | optional | End date — YYYY-MM-DD. Must be ≥ from. |
| page | integer | optional | Page number (default 1). |
Returns 200 OK.
Create an invoice
POST /v1/invoices → Try in Playground
Creates an invoice in draft status. Use Send invoice to email it. The response includes a payment_url the customer can use to pay online.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| customer_name | string | required | Customer full name (max 255 chars). |
| customer_email | string | required | Customer email address. |
| customer_phone | string | optional | Customer phone number. |
| currency | string | optional | One of GEL, USD, EUR, GBP. Defaults to GEL. |
| due_at | date | optional | Payment due date — YYYY-MM-DD. Must be after today. |
| notes | string | optional | Notes shown to the customer on the invoice (max 2000 chars). |
| terms | string | optional | Terms & conditions shown on the invoice (max 2000 chars). |
| discount_type | string | optional | percent or fixed. |
| discount_value | number | optional | Discount amount. Percent: 0–100. Fixed: deducted from subtotal. |
| items | array | required | Line items array — at least one item required. |
items[] object
| Parameter | Type | Description | |
|---|---|---|---|
| description | string | required | Line item description (max 500 chars). |
| quantity | number | required | Quantity (min 0.01 — supports fractional units). |
| unit_price | number | required | Price per unit (min 0). |
{
"customer_name": "Nino Kvaratskhelia",
"customer_email": "[email protected]",
"currency": "GEL",
"due_at": "2026-07-01",
"discount_type": "percent",
"discount_value": 10,
"items": [
{ "description": "Web design — 10 hours", "quantity": 10, "unit_price": 50.00 },
{ "description": "Hosting (monthly)", "quantity": 1, "unit_price": 15.00 }
]
}// 201 Created
{
"uuid": "018f...",
"invoice_number": "INV-0001",
"status": "draft",
"currency": "GEL",
"subtotal": 515.00,
"discount_type": "percent",
"discount_value": 10,
"discount_amount": 51.50,
"total": 463.50,
"due_at": "2026-07-01",
"payment_url": "https://qpy.ge/invoice/018f...",
"items": [
{ "description": "Web design — 10 hours", "quantity": 10, "unit_price": 50.00, "line_total": 500.00 },
{ "description": "Hosting (monthly)", "quantity": 1, "unit_price": 15.00, "line_total": 15.00 }
]
}Get an invoice
GET /v1/invoices/{uuid} → Try in Playground
Returns the full invoice object including payment_url, sent_at, viewed_at, and paid_at timestamps.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Invoice UUID. |
Returns 200 OK.
Response fields
| Field | Type | Description |
|---|---|---|
| uuid | string | Unique invoice identifier. |
| invoice_number | string | Human-readable invoice number (e.g. INV-0001). |
| status | string | draft, sent, viewed, paid, cancelled, overdue. |
| customer_name | string | Customer full name. |
| customer_email | string | Customer email address. |
| customer_phone | string|null | Customer phone number. |
| currency | string | Currency code. |
| subtotal | number | Sum of all line items before discount. |
| discount_type | string|null | percent or fixed. |
| discount_value | number|null | Discount value as entered. |
| discount_amount | number|null | Actual amount deducted. |
| total | number | Final amount after discount. |
| notes | string|null | Notes shown to the customer. |
| terms | string|null | Terms & conditions text. |
| due_at | date|null | Payment due date. |
| created_at | datetime | Invoice creation timestamp. |
| sent_at | datetime|null | When the invoice was emailed to the customer. |
| viewed_at | datetime|null | When the customer first opened the invoice. |
| paid_at | datetime|null | When the invoice was paid. |
| payment_url | string | URL the customer can use to pay online. |
| items | array | Line items (description, quantity, unit_price, line_total). |
Send an invoice
POST /v1/invoices/{uuid}/send → Try in Playground
Emails the invoice to the customer and transitions status to sent. Cannot be called when status is paid or cancelled.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
| uuid | string | required | Invoice UUID. |
// 200 OK
{
"status": "sent",
"sent_at": "2026-06-01T10:05:00+00:00",
"email_delivered": true
}207 Multi-Status if the status update succeeded but the email failed to deliver — check email_delivered in the response.List gateways
GET /v1/gateways → Try in Playground
Returns the active gateways for your brand — gateways with an active module subscription. Useful for populating a gateway selector in your own UI. Requires authentication but does not require a gateway license.
// 200 OK
{
"gateways": [
{
"slug": "bog_card",
"name": "BOG Card",
"min_amount_gel": 0.5,
"max_amount_gel": 50000,
"brand_color": "#FF6600",
"bg_color": "#FFF3E0",
"icon_url": "https://cdn.quickpay.ge/gateways/bog.svg",
"dark_icon_url": "https://cdn.quickpay.ge/gateways/bog-dark.svg"
}
]
}All supported gateways — static reference. GET /v1/gateways returns only the gateways with an active module subscription on your account.
| Slug | Display name | Type | Payment methods |
|---|---|---|---|
| bog_card | BOG Card | Card | Visa/MC, Apple Pay, Google Pay |
| bog_installment | BOG Installment | Installment loan | Card (3–36 month plans) |
| bog_bnpl | BOG Part by Part | BNPL | 0% interest split |
| tbc_card | TBC Card | Card | Visa/MC, Apple Pay, Google Pay |
| tbc_installment | TBC Installment | Installment loan | Card (instalment plans) |
| credo_installment | Credo Installment | Installment loan | Card (instalment plans) |
| flitt | Flitt | Card | Visa/MC |
| flitt_installment | Flitt Installment | Installment loan | Card (instalment plans) |
| flitt_bnpl | Flitt BNPL | BNPL | 0% interest split |
| keepz | Keepz | Multi-method | Card, QR code, open banking, digital wallet |
| citypay | CityPay | Crypto | Cryptocurrency (settled in GEL) |
| unipay | UniPay | Card | Visa/MC |
| fastoo | Fastoo | Card | Visa/MC |
| moka | Moka | Card | Visa/MC |
| paypal | PayPal | PayPal | PayPal balance, card via PayPal |
| paysera | Paysera | Multi-method | Card, open banking |
| bank_transfer | Bank Transfer | Manual | IBAN bank transfer (no hosted page) |
| cash_on_delivery | Cash on Delivery | Offline | No payment collected online |
Apple Pay and Google Pay are offered automatically by BOG and TBC on supported devices — no extra integration required.
List checkout templates
GET /v1/checkout-templates → Try in Playground
Returns the active checkout templates for your brand. Use the returned id as checkout_template_id when creating a payment. Requires authentication but does not require a gateway license.
// 200 OK
{
"templates": [
{
"id": 1,
"uuid": "018f...",
"name": "Default",
"is_default": true,
"gateway_count": 3,
"fields_count": 2
}
]
}Exchange rates
GET /v1/exchange-rates → Try in Playground
Public endpoint — no authentication required. Converts an amount between two currencies using live rates.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| from | string | required | 3-letter source currency code (e.g. GEL). |
| to | string | required | 3-letter target currency code (e.g. USD). |
| amount | number | required | Amount to convert. |
// 200 OK
{
"from": "GEL",
"to": "USD",
"rate": 0.3676,
"amount": 100,
"converted": 36.76
}Installments & BNPL
Trigger installment or BNPL checkout with a single POST /v1/payments — no extra API calls needed. Quickpay fetches available plans from the bank and displays them on the hosted payment page automatically.
Installment slugs: bog_installment · tbc_installment · credo_installment · flitt_installment — customer picks a repayment plan (3–36 months).
BNPL slugs: bog_bnpl (BOG "Part by Part") · flitt_bnpl — 0% interest split; the bank absorbs the financing cost.
- Merchant passes an installment or BNPL
gateway_sluginPOST /v1/payments. - Customer lands on the hosted payment page — Quickpay fetches available plans live and displays them.
- Customer selects a plan and completes checkout at the bank.
- Merchant receives the full payment amount upfront.
- Webhook fires
payment.paid— identical to a card payment.
{
"amount": 999.00,
"currency": "GEL",
"description": "MacBook Pro",
"gateway_slug": "bog_installment",
"return_url": "https://yourshop.ge/thank-you"
}Plan availability and terms are fetched live from each bank's API and cached for 5 minutes. If the bank API is temporarily unavailable, the hosted page shows a graceful "currently unavailable" message.
Recurring Payments
Card-on-file and subscription billing using *_subscriptions gateway slugs. The customer completes the first payment on the hosted page, which charges and tokenises their card with the bank. Subsequent charges happen server-to-server with no customer interaction.
Available recurring slugs: bog_card_subscriptions · tbc_card_subscriptions · flitt_subscriptions · unipay_subscriptions · fastoo_subscriptions · paypal_subscriptions · liberty_card_subscriptions
Step 1 POST /v1/payments { gateway_slug: "bog_card_subscriptions", amount: 29.99 }
→ customer pays on the hosted page
→ payment.paid webhook fires with "subscription_token": "<uuid>" ← save this
Step 2 POST /v1/payments/charge { subscription_token: "<uuid>", amount: 29.99 }
→ 202 Accepted
→ subscription.charged webhook on success (subscription.failed on failure)Charge a subscription token
POST /v1/payments/charge → Try in Playground
Charges a stored subscription token server-to-server. The customer completed the first payment on the hosted page using a *_subscriptions slug, which tokenised their card with the bank.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
| subscription_token | string | required | Token returned in the payment.paid webhook after the first payment (UUID). |
| amount | number | optional | Override amount for this charge. Defaults to the original subscription amount. |
| description | string | optional | Description for this charge (max 500 chars). |
// 202 Accepted
{
"uuid": "018f...",
"status": "pending",
"amount": 29.99,
"currency": "GEL",
"subscription_token": "a1b2c3d4-...",
"message": "Charge initiated. Final result will be delivered via webhook."
}429 charge_too_frequent with a retry_after timestamp.Webhooks
When a payment status changes, Quickpay sends a POST request to your Webhook URL (Dashboard → Brand Settings). Your endpoint must respond with HTTP 2xx within 30 seconds. Failed deliveries are retried up to 5 times with exponential back-off.
Payment events: payment.paid · payment.failed · payment.refunded · payment.partially_refunded · payment.refund_pending · payment.cancelled · payment.expired
Subscription events: subscription.charged · subscription.failed — fired by POST /v1/payments/charge.
Invoice events: invoice.paid — fired when an invoice is paid.
Lead events: lead.submitted — fired when a landing-page lead is captured.
{
"event": "payment.paid",
"uuid": "018f2a3b-...",
"merchant_order_id": "ORD-1234",
"status": "paid",
"amount": 149.99,
"currency": "GEL",
"customer_name": "Giorgi Beridze",
"customer_email": "[email protected]",
"gateway_order_id": "BOG-XYZ789",
"subscription_token": "a1b2c3d4-...",
"paid_at": "2026-05-16T10:23:00Z",
"created_at": "2026-05-16T10:20:00Z",
"form_data": {},
"metadata": {}
}subscription_token is only present when the payment used a *_subscriptions gateway slug. Save it to use with POST /v1/payments/charge.
Signature verification
Every webhook request includes a QUICKPAY-SIGNATURE header. Always verify it to ensure the request is authentic and hasn't been tampered with.
QUICKPAY-SIGNATURE: t=1716123456,v1=abc123def456...[$timestamp, $signature] = explode(",", $request->header("QUICKPAY-SIGNATURE"));
$timestamp = substr($timestamp, 2); // strip "t="
$v1 = substr($signature, 3); // strip "v1="
// Reject stale payloads (> 5 minutes)
if (abs(time() - (int)$timestamp) > 300) {
abort(400, "Webhook timestamp too old");
}
$payload = "{$timestamp}." . $request->getContent();
$expected = hash_hmac("sha256", $payload, $webhookSecret);
if (!hash_equals($expected, $v1)) {
abort(400, "Invalid signature");
}const crypto = require('crypto');
const [ts, sig] = req.headers['quickpay-signature'].split(',');
const timestamp = ts.replace('t=', '');
const v1 = sig.replace('v1=', '');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send('Webhook timestamp too old');
}
const payload = `${timestamp}.${req.rawBody}`;
const expected = crypto.createHmac('sha256', webhookSecret).update(payload).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.status(400).send('Invalid signature');
}import hmac, hashlib, time
header = request.headers.get('QUICKPAY-SIGNATURE', '')
parts = dict(p.split('=', 1) for p in header.split(','))
timestamp, v1 = parts.get('t', ''), parts.get('v1', '')
if abs(time.time() - int(timestamp)) > 300:
return 'Webhook timestamp too old', 400
payload = f"{timestamp}.{request.data.decode()}"
expected = hmac.new(webhook_secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
return 'Invalid signature', 400Error codes
Every API error response includes a machine-readable error_code string alongside the human message. Switch on these stable codes instead of string-matching translated messages.
{ "error_code": "coupon_expired", "message": "This coupon has expired.", "errors": { "coupon_code": ["This coupon has expired."] } }The errors object is only present on 422 validation responses.
| error_code | HTTP | When it occurs | What to do |
|---|---|---|---|
| missing_api_key | 401 | No Authorization header | Add an Authorization: Bearer qpk_live_... header |
| invalid_api_key | 401 | Key does not exist or was revoked | Check your API key in Dashboard → API Keys |
| account_suspended | 403 | Merchant account is suspended | Contact Quickpay support |
| test_key_live_mode | 403 | Test key used on a live-mode brand | Use your live key or enable test mode on the brand |
| live_key_test_mode | 403 | Live key used on a test-mode brand | Use your test key or switch the brand to live mode |
| no_gateway_license | 403 | No active module subscription for this gateway | Subscribe to the gateway module in Dashboard → Marketplace |
| idempotency_conflict | 409 | Same Idempotency-Key with a different amount or currency | Use a new key, or keep amount/currency unchanged on retry |
| not_found | 404 | Resource does not exist | Check the UUID or slug |
| validation_failed | 422 | Request payload failed validation | Inspect the errors object and correct the offending fields |
| rate_limit_exceeded | 429 | More than 100 req/min per key | Retry after the Retry-After header interval |
| gateway_unavailable | 500 | Gateway API is temporarily down | Retry with exponential back-off |
| coupon_not_found | 422 | Coupon code does not exist | Check the coupon code value |
| coupon_inactive | 422 | Coupon is disabled | Contact merchant support |
| coupon_not_started | 422 | Coupon start date has not arrived | Use the coupon after its start date |
| coupon_expired | 422 | Coupon has expired | Use a different coupon code |
| coupon_product_mismatch | 422 | Coupon is only valid for a different product | Use the correct product |
| coupon_below_min_amount | 422 | Order amount is below the coupon minimum | Increase the order amount |
| coupon_max_uses_reached | 422 | Coupon has been fully redeemed | Coupon is no longer available |
| coupon_per_customer_limit | 422 | Customer has already used this coupon | Use a different coupon |
| coupon_code_taken | 422 | A coupon with this code already exists for the brand | Choose a different, unused coupon code |
| line_items_mismatch | 422 | Sum of line_items totals ≠ amount | Fix line items so their totals match the payment amount |
| template_not_found | 422 | checkout_template_id does not exist | Use a valid ID from GET /v1/checkout-templates |
| select_options_required | 422 | select/radio field missing its options array | Add an options array (with label and value keys) to the field schema |
| amount_or_product_required | 422 | Neither amount nor product_uuid was provided | Include one of amount or product_uuid |
| refund_exceeds_remaining | 422 | Refund amount > remaining refundable amount | Check refunded_amount in the payment object |
| payment_not_refundable | 422 | Payment status does not allow refunds | Only paid and partially_refunded payments can be refunded |
| payment_not_cancellable | 422 | Payment status does not allow cancellation | Only pending, initiated, and transfer_pending payments can be cancelled |
| gateway_refund_unsupported | 422 | This gateway does not support API refunds | Issue the refund manually via the bank's merchant portal |
| invoice_not_sendable | 422 | Invoice is paid or cancelled | Invoices with status draft, sent, viewed, or overdue can be (re)sent |
| subscription_not_found | 404 | subscription_token does not exist or belongs to a different brand | Check the token from the original payment.paid webhook |
| subscription_inactive | 422 | Subscription is cancelled or has expired | Only active subscriptions can be charged |
| subscription_already_cancelled | 422 | Subscription is already cancelled | No action needed — the subscription is already cancelled |
| charge_too_frequent | 429 | A charge was already initiated within the last 24 hours | Retry after the retry_after timestamp in the response body |
| webhook_not_configured | 422 | No webhook URL is configured for the brand | Set a webhook URL with PUT /v1/webhooks before testing or rotating the secret |