↓ Download .md OpenAPI ↗

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.

ℹ For all flows the integration is identical: POST /payments → redirect to 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_installment

0️⃣ 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_delivery

Quick 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.

ℹ Using test mode? Use your 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_here

Rate 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-1

Amounts & 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.

CodeMeaning
401Missing or invalid API key
403Account suspended, test key on live brand, or no active gateway subscription
404Resource not found
409Idempotency key conflict (different amount or currency)
422Validation failed — see the errors object
429Rate limit or charge-frequency exceeded — Retry-After present
500Server 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.

SDKPackageInstallRepository
PHP SDKquickpayge/php-sdkcomposer require quickpayge/php-sdkquickpay.ge/php-sdk · GitHub
Laravel Packagequickpayge/laravelcomposer require quickpayge/laravelquickpay.ge/laravel-package · GitHub
Node.js / TypeScript SDK@quickpay/jsnpm install @quickpay/jsquickpay.ge/node-sdk · GitHub

MCP Server (for AI Agents)

Building with an AI coding agent (Claude, Cursor, and others)? https://mcp.quickpay.ge is a public, no-auth Model Context Protocol server — a live reference your agent can query directly instead of copy-pasting these docs into its context.

{
  "mcpServers": {
    "quickpay": {
      "url": "https://mcp.quickpay.ge"
    }
  }
}
Tool / ResourceWhat it does
search-docsSearch across this reference and the integration guides
get-endpoint-referenceExact request/response fields for one endpoint
get-webhook-signature-exampleWebhook signature verification code (PHP / Node / Python)
list-gatewaysSupported Georgian payment gateways
create-test-paymentCreate a sandbox payment end-to-end (requires a qpk_test_... key)
quickpay://docs/api-reference, quickpay://docs/openapi, quickpay://docs/rag/{topic}This same reference, the OpenAPI spec, and per-topic integration guides, as MCP resources

All tools are read-only except create-test-payment, which only ever creates sandbox data behind a qpk_test_... key — no real money moves through this server. Looking for an AI agent that can manage your live account (create payments, issue refunds, update products)? That's a separate OAuth-authenticated server — see Dashboard → Integrations → Connected AI Apps.

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.

PlatformTypePage
WooCommerce (WordPress)E-commercequickpay.ge/wordpress-plugin
WHMCSHosting / billingquickpay.ge/whmcs-plugin
OpenCartE-commercequickpay.ge/opencart-plugin
CS-CartE-commercequickpay.ge/cscart-plugin
PrestaShopE-commercequickpay.ge/prestashop-plugin
Easy Digital Downloads (WordPress)Digital goodsquickpay.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

ParameterTypeDescription
amountnumberrequiredPayment amount as a decimal (e.g. 49.99). Min 0.01, up to 2 decimal places.
currencystringoptionalOne of GEL, USD, EUR, GBP. Defaults to GEL.
descriptionstringoptionalOrder description shown to the customer (max 500 chars).
gateway_slugstringoptionalCreate the order directly at one gateway and restrict the checkout page to it only — use this when the customer already chose a method on your own site (e.g. separate 'Pay with TBC Installment' / 'Pay with Keepz' buttons). Omit to let the customer choose on the hosted checkout page. Mutually exclusive with preselected_gateway. See List gateways and What you can build for valid slugs.
preselected_gatewaystringoptionalHighlight one gateway as the default choice on the checkout page while still showing every other available gateway — the customer can switch. Only valid on gateway-less payments (omit gateway_slug). Mutually exclusive with gateway_slug.
merchant_order_idstringoptionalYour own order reference — stored and returned on all responses (max 255 chars).
bank_transfer_referencestringoptionalCustom 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_namestringoptionalCustomer full name — pre-fills the checkout form (max 255 chars).
customer_emailstringoptionalCustomer email — pre-fills the form and used for the receipt.
customer_phonestringoptionalCustomer phone — pre-fills the checkout form.
return_urlstringoptionalRedirect URL after checkout completes.
webhook_urlstringoptionalWebhook 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_atdatetimeoptionalISO 8601 datetime after which the payment link expires. Must be in the future.
coupon_codestringoptionalCoupon 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_idintegeroptionalInternal product ID to attach to the payment (used for coupon product-scoping).
product_image_urlstringoptionalProduct image URL shown on the checkout page.
line_itemsarrayoptionalItemised breakdown. The sum of line_items[].total must equal amount.
checkout_template_idintegeroptionalOverride the checkout page template for this payment. Use an id from List checkout templates.
custom_fields_schemaarrayoptionalExtra form fields shown to the customer — see the schema below.
prefill_onlybooleanoptionalWhen true, pre-fill customer info but still show the form for confirmation.
sourcestringoptionalIntegration source tag: woocommerce, whmcs, opencart, cscart, prestashop, edd, ecwid, or api.
idempotency_keystringoptionalAlternative to the Idempotency-Key header (max 255 chars).
tokenize_onlybooleanoptionalSave the customer's card without charging anything. Only valid with a *_subscriptions gateway_slug (excluding paypal_subscriptions) — amount is forced to 0 server-side regardless of what is posted. See the note below.

line_items[] object

ParameterTypeDescription
typestringrequiredOne of product, shipping, tax, fee, discount.
namestringrequiredLine item label in English (max 255 chars).
name_kastringoptionalLine item label in Georgian (max 255 chars).
qtyintegerrequiredQuantity (min 1).
unit_pricenumberrequiredPrice per unit.
totalnumberrequiredLine total. The sum of every total must equal amount.
skustringoptionalProduct SKU (max 100 chars).

custom_fields_schema[] object

ParameterTypeDescription
labelstringrequiredField label shown to the customer (max 200 chars).
typestringrequiredOne of text, textarea, select, number, checkbox, radio.
requiredbooleanoptionalWhether the customer must fill this field.
optionsarrayoptionalRequired 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
}
✓ When gateway_slug is omitted the customer picks the gateway on the hosted page, and the response omits discount_gel, coupon_code, and redirect_url.
✓ Replaying the same 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.
✓ tokenize_only: true saves the customer's card on file without charging anything — the response status resolves to paid once the bank confirms the card save, and payment.paid fires on the webhook exactly like a normal charge, but no funds move. Requires a *_subscriptions gateway_slug (not paypal_subscriptions); anything else returns 422 tokenize_only_gateway_unsupported. Some gateways (bog_card_subscriptions, flitt_subscriptions, unipay_subscriptions, fastoo_subscriptions) may reject a zero-amount save — that comes back as 422 zero_amount_not_supported.

Get a payment

GET /v1/payments/{uuid} → Try in Playground

Returns the full payment object.

Path parameters

ParameterTypeDescription
uuidstringrequiredPayment 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

FieldTypeDescription
uuidstringUnique payment identifier (UUID v7).
merchant_order_idstring|nullYour order reference passed at creation.
statusstringPayment status — see the list below.
amountnumberCharged amount.
currencystringCurrency code.
descriptionstring|nullOrder description.
customer_namestring|nullCustomer full name.
customer_emailstring|nullCustomer email address.
gateway_order_idstring|nullOrder ID assigned by the payment gateway.
bank_transfer_referencestring|nullReference shown to the customer for Bank Transfer payments — your requested value, or a Quickpay-generated code if none was provided.
surchargenumber|nullGateway surcharge applied, if any.
couponobject|nullApplied coupon details (code, type, discount_gel).
form_dataobjectCustom field values submitted by the customer.
paid_atdatetime|nullISO 8601 timestamp when payment was confirmed.
expires_atdatetime|nullISO 8601 expiry timestamp.
created_atdatetimeISO 8601 creation timestamp.
line_itemsarray|nullItemised breakdown, if provided at creation.
refunded_amountnumberTotal amount refunded so far.
refundsarrayRefund records (uuid, amount, reason, status, initiated_by, created_at).
✓ Payment statuses: 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

ParameterTypeDescription
statusstringoptionalFilter by status — see the status list above.
gatewaystringoptionalFilter by gateway slug (e.g. bog_card, tbc_card).
fromdateoptionalStart date — YYYY-MM-DD.
todateoptionalEnd date — YYYY-MM-DD. Must be ≥ from.
pageintegeroptionalPage 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

ParameterTypeDescription
uuidstringrequiredPayment UUID.

Body parameters

ParameterTypeDescription
amountnumberoptionalAmount to refund. Defaults to the full remaining refundable amount.
reasonstringoptionalInternal reason for the refund — stored in the audit log (max 500 chars).
{ "amount": 50.00, "reason": "Customer requested partial refund" }

Returns 200 OK.

✓ Returns the full payment object with an updated 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

ParameterTypeDescription
activebooleanoptionaltrue or false — filter by active status.
pageintegeroptionalPage 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

ParameterTypeDescription
name_enstringoptionalProduct name in English (required if name_ka is absent).
name_kastringoptionalProduct name in Georgian (required if name_en is absent).
short_description_enstringoptionalShort description in English.
short_description_kastringoptionalShort description in Georgian.
pricenumberrequiredSale price (min 0.01).
compare_pricenumberoptionalOriginal/crossed-out price shown to highlight a discount.
base_currencystringoptionalBase pricing currency. One of GEL, USD, EUR, GBP. Defaults to GEL.
accepted_currenciesarrayoptionalCurrencies the customer may pay in (e.g. ["GEL","USD"]).
skustringoptionalStock keeping unit identifier (max 100 chars).
track_stockbooleanoptionalEnable stock tracking. Defaults to false.
stock_quantityintegeroptionalCurrent stock level. Only used when track_stock is true.
categoriesarrayoptionalArray of category strings (e.g. ["clothing","sale"]).
custom_fields_schemaarrayoptionalExtra checkout form fields — same structure as the payment custom_fields_schema.
is_activebooleanoptionalWhether the product is available for purchase. Defaults to true.

Returns 201 Created.

✓ Returns the full product object (same fields as Get a product).

Get a product

GET /v1/products/{uuid} → Try in Playground

Returns the full product object.

Path parameters

ParameterTypeDescription
uuidstringrequiredProduct UUID.

Returns 200 OK.

Response fields

FieldTypeDescription
uuidstringUnique product identifier.
name_enstring|nullProduct name in English.
name_kastring|nullProduct name in Georgian.
short_description_enstring|nullShort description in English.
short_description_kastring|nullShort description in Georgian.
pricenumberSale price.
compare_pricenumber|nullOriginal/crossed-out price.
base_currencystringBase pricing currency.
accepted_currenciesarrayCurrencies the customer may pay in.
skustring|nullStock keeping unit.
track_stockbooleanWhether stock tracking is enabled.
stock_quantityinteger|nullCurrent stock level.
image_urlstring|nullPrimary product image URL.
categoriesarrayCategory strings.
custom_fields_schemaarrayExtra checkout fields.
is_activebooleanWhether the product is available.
created_atdatetimeCreation timestamp.
updated_atdatetimeLast 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

ParameterTypeDescription
uuidstringrequiredProduct UUID.

Returns 200 OK.

Delete a product

DELETE /v1/products/{uuid} → Try in Playground

Deletes the product.

Path parameters

ParameterTypeDescription
uuidstringrequiredProduct UUID.
// 200 OK
{ "deleted": true }

POST /v1/checkout-links → Try in Playground

Creates a reusable payment link. Provide product_uuid (price comes from the product), a fixed amount, or neither for a variable-amount link — the customer picks/enters the amount on the payment page (donations, tips, pay-what-you-want).

Body parameters

ParameterTypeDescription
product_uuidstringoptionalAttach an existing product — its price is used automatically.
amountnumber|nulloptionalFixed amount in the chosen currency. Omit (with no product_uuid) to create a variable-amount link.
preset_amountsnumber[]optionalVariable-amount links only (no product_uuid/amount). Up to 6 quick-pick amounts shown as buttons on the payment page.
allow_custom_amountbooleanoptionalVariable-amount links only. Whether the customer can also type a custom amount. Defaults to true.
currency_overridestringoptionalOverride the default currency: GEL, USD, EUR, or GBP.
hide_currency_selectorbooleanoptionalHide the currency selector on checkout. Defaults to false.
expires_atdatetimeoptionalISO 8601 datetime after which the link is no longer usable.
max_usesintegeroptionalMaximum number of times the link can be used (min 1).
allowed_gatewaysarrayoptionalRestrict which gateway slugs are shown. Defaults to all active gateways.
custom_fields_schemaarrayoptionalExtra checkout form fields (see note below).
checkout_settingsobjectoptionalCheckout 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>
AttributeDefaultDescription
data-checkout-link—Required. Checkout link UUID/slug or a full https:// URL.
data-labelგადახდაButton text.
data-color#2563ebButton background color.
data-text-color#ffffffButton text color.
data-sizemdsm, md, or lg.
data-full-widthfalseSet to true for a block button that stretches to the container width.
data-radius8pxCSS border-radius value.
data-target_selfUse _blank to open checkout in a new tab with rel="noopener noreferrer".
ℹ If your site uses Content Security Policy, allow the Quickpay script host, for example: 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

ParameterTypeDescription
statusstringoptionalOne of draft, sent, viewed, paid, cancelled, overdue.
customer_emailstringoptionalFilter by exact customer email.
fromdateoptionalStart date — YYYY-MM-DD.
todateoptionalEnd date — YYYY-MM-DD. Must be ≥ from.
pageintegeroptionalPage 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

ParameterTypeDescription
customer_namestringrequiredCustomer full name (max 255 chars).
customer_emailstringrequiredCustomer email address.
customer_phonestringoptionalCustomer phone number.
currencystringoptionalOne of GEL, USD, EUR, GBP. Defaults to GEL.
due_atdateoptionalPayment due date — YYYY-MM-DD. Must be after today.
notesstringoptionalNotes shown to the customer on the invoice (max 2000 chars).
termsstringoptionalTerms & conditions shown on the invoice (max 2000 chars).
discount_typestringoptionalpercent or fixed.
discount_valuenumberoptionalDiscount amount. Percent: 0–100. Fixed: deducted from subtotal.
itemsarrayrequiredLine items array — at least one item required.

items[] object

ParameterTypeDescription
descriptionstringrequiredLine item description (max 500 chars).
quantitynumberrequiredQuantity (min 0.01 — supports fractional units).
unit_pricenumberrequiredPrice 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

ParameterTypeDescription
uuidstringrequiredInvoice UUID.

Returns 200 OK.

Response fields

FieldTypeDescription
uuidstringUnique invoice identifier.
invoice_numberstringHuman-readable invoice number (e.g. INV-0001).
statusstringdraft, sent, viewed, paid, cancelled, overdue.
customer_namestringCustomer full name.
customer_emailstringCustomer email address.
customer_phonestring|nullCustomer phone number.
currencystringCurrency code.
subtotalnumberSum of all line items before discount.
discount_typestring|nullpercent or fixed.
discount_valuenumber|nullDiscount value as entered.
discount_amountnumber|nullActual amount deducted.
totalnumberFinal amount after discount.
notesstring|nullNotes shown to the customer.
termsstring|nullTerms & conditions text.
due_atdate|nullPayment due date.
created_atdatetimeInvoice creation timestamp.
sent_atdatetime|nullWhen the invoice was emailed to the customer.
viewed_atdatetime|nullWhen the customer first opened the invoice.
paid_atdatetime|nullWhen the invoice was paid.
payment_urlstringURL the customer can use to pay online.
itemsarrayLine 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

ParameterTypeDescription
uuidstringrequiredInvoice UUID.
// 200 OK
{
  "status": "sent",
  "sent_at": "2026-06-01T10:05:00+00:00",
  "email_delivered": true
}
✓ Returns 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.

SlugDisplay nameTypePayment methods
bog_cardBOG CardCardVisa/MC, Apple Pay, Google Pay
bog_installmentBOG InstallmentInstallment loanCard (3–36 month plans)
bog_bnplBOG Part by PartBNPL0% interest split
tbc_cardTBC CardCardVisa/MC, Apple Pay, Google Pay
tbc_installmentTBC InstallmentInstallment loanCard (instalment plans)
credo_installmentCredo InstallmentInstallment loanCard (instalment plans)
flittFlittCardVisa/MC
flitt_installmentFlitt InstallmentInstallment loanCard (instalment plans)
flitt_bnplFlitt BNPLBNPL0% interest split
keepzKeepzMulti-methodCard, QR code, open banking, digital wallet
citypayCityPayCryptoCryptocurrency (settled in GEL)
unipayUniPayCardVisa/MC
fastooFastooCardVisa/MC
mokaMokaCardVisa/MC
paypalPayPalPayPalPayPal balance, card via PayPal
payseraPayseraMulti-methodCard, open banking
liberty_cardLiberty CardCardVisa/MC via Liberty Bank
dodo_paymentsDodo PaymentsMerchant of RecordGlobal card/wallet payments, Dodo handles tax compliance
bank_transferBank TransferManualIBAN bank transfer (no hosted page)
cash_on_deliveryCash on DeliveryOfflineNo 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

ParameterTypeDescription
fromstringrequired3-letter source currency code (e.g. GEL).
tostringrequired3-letter target currency code (e.g. USD).
amountnumberrequiredAmount 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.

✓ Merchants receive the full amount upfront. The bank collects instalments from the customer — this is not a deferred payout.

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.

  1. Merchant passes an installment or BNPL gateway_slug in POST /v1/payments.
  2. Customer lands on the hosted payment page — Quickpay fetches available plans live and displays them.
  3. Customer selects a plan and completes checkout at the bank.
  4. Merchant receives the full payment amount upfront.
  5. 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.

Get installment plans

GET /v1/installment-plans/{gateway_slug} → Try in Playground

Fetches real installment/BNPL plan numbers for a gateway before redirecting the customer — same live calculator POST /v1/payments uses internally on the hosted page, exposed directly for integrators who need to show plan terms on their own checkout UI first.

Path parameters

ParameterTypeDescription
gateway_slugstringrequiredOne of bog_installment, bog_bnpl, tbc_installment, credo_installment, flitt_installment.

Query parameters

ParameterTypeDescription
amountnumberrequiredOrder amount as a decimal (e.g. 149.99). Min 0.01.
// 200 OK
{
  "supports_installments": true,
  "plans": [
    { "months": 3, "monthly_payment": 50.33, "total": 150.99, "discount_code": "STANDARD", "label": "3 თვე — 50.33 ₾/თვე" }
  ]
}
✓ For tbc_installment, credo_installment, and flitt_installment — gateways with no public calculator API — the response is { "supports_installments": true, "plans": null, "message": "Plan terms are shown after continuing to the bank's page." }.
✓ For a gateway that does not support installments at all, the response is { "supports_installments": false, "plans": null }.
✓ Results are cached for 5 minutes per (brand, gateway_slug, amount). If the bank's calculator API is temporarily unavailable, a stale cached result is served with "stale": true when one exists.

Requires an active gateway license the same way POST /v1/payments does — the license check is domain-wide, not per gateway.

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 · dodo_payments_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

ParameterTypeDescription
subscription_tokenstringrequiredToken returned in the payment.paid webhook after the first payment (UUID).
amountnumberoptionalOverride amount for this charge. Defaults to the original subscription amount.
descriptionstringoptionalDescription 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."
}
✓ A minimum of 24 hours must pass between charges on the same token, otherwise the API returns 429 charge_too_frequent with a retry_after timestamp.
ℹ Looking for fully managed subscription scheduling? The Subscriptions dashboard module handles billing cycles, dunning, and renewals automatically — no API calls needed.

Zero-Amount Tokenization

Save a customer's card on file without charging anything — useful for signing a customer up before their first real charge is known (e.g. a free trial, or a plan the customer picks later). Pass tokenize_only: true on POST /v1/payments with a *_subscriptions gateway_slug. amount is forced to 0 server-side regardless of what you post.

{
  "gateway_slug": "tbc_card_subscriptions",
  "tokenize_only": true,
  "customer_email": "[email protected]"
}

Supported gateways: bog_card_subscriptions · tbc_card_subscriptions · flitt_subscriptions · unipay_subscriptions · fastoo_subscriptions · liberty_card_subscriptions · dodo_payments_subscriptions. paypal_subscriptions is plan/billing-agreement based, not a generic saved-card token, so it always returns 422 tokenize_only_gateway_unsupported.

The customer completes the save-card step on the hosted page exactly like a normal first *_subscriptions payment. Once the bank confirms the card is saved, the payment resolves to status: "paid" (amount 0) and payment.paid fires on the webhook with a subscription_token — use it with POST /v1/payments/charge for the customer's first real charge.

✓ A handful of gateways (bog_card_subscriptions, flitt_subscriptions, unipay_subscriptions, fastoo_subscriptions) may reject a zero-amount card save at the bank's discretion — that comes back as 422 zero_amount_not_supported rather than a generic failure. If you hit this consistently on a gateway, fall back to a small non-zero first charge instead.

Managing subscriptions

Full lifecycle management for CustomerSubscriptions beyond the basic recurring-charge flow above — list/filter, merchant-initiated creation, cancel (immediate or at period end), pause/resume, reactivate, and a paginated audit trail. See also Zero-Amount Tokenization for saving a card without a subscription.

List subscriptions

GET /v1/subscriptions → Try in Playground

Lists your brand's recurring subscriptions, newest first.

Query parameters

ParameterTypeDescription
statusstringoptionalFilter by one of active, past_due, cancelled, expired, paused, pending_cancellation.
fromdateoptionalOnly subscriptions created on/after this date.
todateoptionalOnly subscriptions created on/before this date.
customer_idintegeroptionalFilter to one customer.
customer_emailstringoptionalFilter to one customer by email.
plan_uuidstringoptionalFilter to subscriptions currently on one plan.
// 200 OK
{
  "data": [
    {
      "uuid": "018f...",
      "plan_uuid": "018f...",
      "customer_id": 42,
      "customer_name": "Giorgi Beridze",
      "customer_email": "[email protected]",
      "customer_phone": "+995555000000",
      "amount": 29.99,
      "currency": "GEL",
      "status": "active",
      "gateway_slug": "bog_card_subscriptions",
      "billing_interval_unit": "month",
      "billing_interval_count": 1,
      "billing_anchor_day": null,
      "current_period_start": "2026-08-16T10:20:00Z",
      "current_period_end": "2026-09-16T10:20:00Z",
      "next_billing_date": "2026-09-16T10:20:00Z",
      "last_charge_at": "2026-08-16T10:20:00Z",
      "cancelled_at": null,
      "created_at": "2026-05-16T10:20:00Z"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 }
}

Create a subscription (merchant-initiated)

POST /v1/subscriptions → Try in Playground

Starts a recurring subscription for a customer who already has a saved payment method — e.g. a phone-order subscription. This never collects card details itself: create the payment method first with Add a payment method. No initial charge is made; the first charge happens at the plan's next period end via the normal renewal cycle, exactly like a subscription created through the hosted checkout.

Body parameters

ParameterTypeDescription
plan_uuidstringrequiredThe SubscriptionPlan to subscribe onto — see List subscription plans.
customer_idintegerrequiredThe customer to subscribe.
payment_method_uuidstringoptionalA specific saved payment method to charge. Defaults to the customer's default active payment method for this brand.
// 201 Created
{
  "uuid": "018f...",
  "plan_uuid": "018f...",
  "customer_id": 42,
  "status": "active",
  "amount": 29.99,
  "currency": "GEL",
  "gateway_slug": "bog_card_subscriptions",
  "current_period_start": "2026-09-16T10:20:00Z",
  "current_period_end": "2026-10-16T10:20:00Z"
}
✓ Fires a subscription.created webhook.
✓ Returns 404 subscription_plan_not_found for an unknown or archived plan, 404 customer_not_found for an unknown customer, 404 payment_method_not_found for an unknown payment_method_uuid, and 422 no_active_payment_method when the customer has no saved payment method at all.

Get a subscription

GET /v1/subscriptions/{uuid} → Try in Playground

Returns a single subscription by UUID.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Returns 200 OK.

List subscription events

GET /v1/subscriptions/{uuid}/events → Try in Playground

The subscription's full audit trail, paginated, newest first — every charge, failure, cancellation, pause/resume, plan change, payment-method update, and reactivation.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.
// 200 OK
{
  "data": [
    {
      "id": 501,
      "event_type": "plan_changed",
      "actor_type": "merchant",
      "actor_id": 7,
      "payload": { "from_plan_id": 12, "to_plan_id": 13, "mode": "immediate", "amount": 10.0 },
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 }
}
✓ event_type is one of created, charged, charge_failed, cancelled, paused, resumed, plan_changed, payment_method_updated, reactivated.

Cancel a subscription

POST /v1/subscriptions/{uuid}/cancel → Try in Playground

Cancels a subscription, immediately by default.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Body parameters

ParameterTypeDescription
at_period_endbooleanoptionalWhen true, the subscription keeps its access and billing period but stops renewing — status becomes pending_cancellation and it is finalized to cancelled once current_period_end passes. Ignored (cancels immediately) if the current period has already ended.
reasonstringoptionalOptional cancellation reason, stored on the subscription and its audit trail (max 500 chars).

Returns 200 OK.

✓ Returns 422 subscription_already_cancelled for a subscription that is already cancelled or pending_cancellation.

Reactivate a subscription

POST /v1/subscriptions/{uuid}/reactivate → Try in Playground

Undoes a cancellation — moves a cancelled, past_due, or pending_cancellation subscription back to active.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Returns 200 OK.

✓ Returns 422 subscription_reactivation_not_allowed for an active, paused, or expired subscription — an expired subscription never had a payment method attached, so there is nothing to reactivate.

Pause a subscription

POST /v1/subscriptions/{uuid}/pause → Try in Playground

Pauses an active subscription — no further charges happen while paused.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Body parameters

ParameterTypeDescription
resumes_atdatetimeoptionalWhen set, a future date to automatically resume. Omit to pause indefinitely until Resume a subscription is called.

Returns 200 OK.

✓ Fires a subscription.paused webhook.
✓ Only an active subscription can be paused — returns 422 subscription_inactive otherwise.

Resume a subscription

POST /v1/subscriptions/{uuid}/resume → Try in Playground

Resumes a paused subscription back to active.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Returns 200 OK.

✓ Fires a subscription.resumed webhook.
✓ Only a paused subscription can be resumed — returns 422 subscription_not_paused otherwise.

Plan changes & payment methods on file

Move a subscription onto a different SubscriptionPlan with configurable proration, or hand the customer a hosted link to update their card or manage everything themselves.

Get an update-card link

POST /v1/subscriptions/{uuid}/update-payment-method → Try in Playground

Mints a one-time hosted link that walks the customer through re-entering their card on the bank's own checkout page. On completion, the subscription is repointed at the newly-saved CustomerPaymentMethod.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.
// 200 OK
{
  "update_url": "https://qpy.ge/update-card/eyJpdiI6...",
  "expires_at": "2026-09-18T10:20:00Z"
}
✓ Fires a subscription.payment_method_updated webhook once the customer completes the flow.
✓ Only works for a *_subscriptions gateway (never paypal_subscriptions) — returns 422 tokenize_only_gateway_unsupported otherwise, or 422 zero_amount_not_supported for a gateway that has not confirmed zero-amount card saves.

Preview a plan change

POST /v1/subscriptions/{uuid}/preview-plan-change → Try in Playground

Computes what Change plan would charge or credit, without changing anything — build an upgrade/downgrade confirmation screen against this.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Body parameters

ParameterTypeDescription
plan_uuidstringrequiredThe target SubscriptionPlan.
modestringoptionalOne of immediate, deferred, none. Defaults to the target plan's own proration_mode.
// 200 OK
{
  "mode": "immediate",
  "amount": 10.0,
  "currency": "GEL",
  "from_plan_uuid": "018f...",
  "to_plan_uuid": "018f...",
  "remaining_days": 15,
  "total_days": 30
}
✓ Plan changes are same-currency only — returns 422 plan_currency_mismatch otherwise.

Change plan

POST /v1/subscriptions/{uuid}/change-plan → Try in Playground

Moves a subscription onto a different plan. immediate mode charges/credits the prorated difference right away; deferred folds it into the next renewal charge; none takes effect at the next renewal with no math. Every change writes its own auditable proration record regardless of mode.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Body parameters

ParameterTypeDescription
plan_uuidstringrequiredThe target SubscriptionPlan.
modestringoptionalOne of immediate, deferred, none. Defaults to the target plan's own proration_mode.
// 200 OK
{
  "subscription": { "uuid": "018f...", "status": "active", "amount": 39.99 },
  "proration": {
    "id": 501,
    "mode": "immediate",
    "amount": 10.0,
    "from_plan_id": 12,
    "to_plan_id": 13,
    "applied_at": "2026-09-01T09:00:00Z",
    "applied_to_payment_id": 9001,
    "created_at": "2026-09-01T09:00:00Z"
  }
}
✓ Fires a subscription.plan_changed webhook.
✓ A downgrade credit (negative immediate amount) is never auto-refunded — it is recorded for audit only.

Get a hosted portal link

POST /v1/subscriptions/{uuid}/portal-session → Try in Playground

Mints a qpy.ge/manage/{token} hosted link — point your own "Manage Subscription" button at the returned url (Hybrid tier) rather than building subscription-management UI yourself.

Path parameters

ParameterTypeDescription
uuidstringrequiredSubscription UUID.

Body parameters

ParameterTypeDescription
scopestringoptionalsubscription (default) scopes the link to just this one, all widens it to every subscription sharing this customer's email on this brand.
ttl_daysintegeroptionalLink lifetime in days, 1–365. Defaults to 30.
// 201 Created
{
  "url": "https://qpy.ge/manage/9f8c...",
  "scope": "subscription",
  "expires_at": "2026-10-16T10:20:00Z"
}

Recipes: letting a customer manage their own subscription

Every *_subscriptions gateway requires the customer to be able to view and cancel their own subscription — this traces back to a National Bank of Georgia rule on recurring payments, and every Quickpay-hosted subscription notice email already links to it. You have two ways to offer this, and you can mix them per subscription — nothing below needs picking once for the whole integration.

Hosted portal (recipe A)Build your own (recipe B)
You writeOne API call + a link/buttonA UI against the full subscription API
Quickpay hostsThe entire page (qpy.ge/manage/{token})Nothing — you call the API directly
BrandingYour brand's logo/colors, no Quickpay UI to themeFully yours
Best forMerchants who don't want to build subscription UI at allMerchants with an existing customer dashboard
Also seemy.qpy.ge — a free, zero-code alternative: any customer can self-register there and see every subscription linked to their email across every merchant on Quickpay, with no integration work at all—

Recipe A — use the hosted portal

Mint a signed link and point a "Manage Subscription" button at it. Quickpay renders the whole page — plan, next billing date, charge history, and cancel/update-card actions — themed with your brand's logo and colors. You never see the customer's card details or session state.

  1. Call POST /v1/subscriptions/{uuid}/portal-session (see Plan changes & payment methods on file above) whenever you want to send the customer a management link — from your own dashboard, a support macro, an SMS, or a "Manage my subscription" email you send yourself.
  2. Pass "scope": "subscription" (default) for a link scoped to just that one subscription, or "scope": "all" to cover every subscription sharing that customer's email on your brand — useful for a customer with more than one active subscription.
  3. The response is { "url": "https://qpy.ge/manage/<token>", "expires_at": "..." }. The link is multi-use until it expires (ttl_days, default 30) — send the same one in a follow-up reminder rather than minting a new one each time.
  4. Nothing else to build: the page itself handles cancel and "update payment method" (which redirects into the same zero-amount re-tokenization flow as Zero-Amount Tokenization).
POST /v1/subscriptions/f47ac10b-58cc-4372-a567-0e02b2c3d479/portal-session
{
  "scope": "subscription",
  "ttl_days": 30
}

→ 201 Created
{
  "url": "https://qpy.ge/manage/qpm_9f8e7d6c5b4a...",
  "scope": "subscription",
  "expires_at": "2026-10-17T09:00:00Z"
}
✓ This is exactly what every compliance notice email (the "you'll be charged in 4 weeks" / trial-ending reminders) links to automatically — you don't need to wire anything for those; they mint their own portal session behind the scenes.
⚠ This is not Laravel's signed-URL mechanism and not a JWT — it's an opaque, randomly-generated token, only its hash ever stored server-side (the same pattern as your API keys). Treat the URL itself as a bearer credential: don't log it anywhere a support agent or analytics tool would see it in plain text, and don't embed it in a public page.

Recipe B — build your own subscription portal

For a merchant with an existing logged-in customer dashboard who wants subscription management to feel native — same layout, same session, no redirect to a Quickpay-hosted page. Call the API directly for every action; nothing is hosted for you.

  1. On your customer's account/orders page, call GET /v1/subscriptions?customer_email=... (or customer_id) to list what they have. Each row includes status, amount, current_period_end, and the plan.
  2. For the detail view, GET /v1/subscriptions/{uuid} for the current state and GET /v1/subscriptions/{uuid}/events for the paginated audit trail (created, charged, charge_failed, cancelled, paused, resumed, plan_changed, payment_method_updated) — enough to render a full timeline without your own event log.
  3. "Cancel" → POST /v1/subscriptions/{uuid}/cancel. Pass "at_period_end": true for a "cancel at the end of the current billing period" option (access continues, no further charge) instead of immediate cancellation.
  4. "Pause" / "Resume" → POST /v1/subscriptions/{uuid}/pause (optionally with resumes_at for a scheduled auto-resume) and POST /v1/subscriptions/{uuid}/resume.
  5. "Change plan" → POST /v1/subscriptions/{uuid}/preview-plan-change to show the customer the exact prorated amount before they confirm, then POST /v1/subscriptions/{uuid}/change-plan to apply it.
  6. "Update card" → you still don't re-implement tokenization yourself: call POST /v1/subscriptions/{uuid}/update-payment-method, which returns a one-time hosted redirect URL to the bank's own card-entry page. Send the customer there (a new tab or a WebView), and the card is vaulted on return — same mechanism recipe A's portal uses internally.
✓ paypal_subscriptions has a reduced surface here by design — PayPal owns its own billing schedule, so pause/resume/preview-plan-change/change-plan all return 422 paypal_subscription_action_unsupported for it. cancel still works normally and actually calls PayPal's own subscription-cancel API so billing stops for real, not just in Quickpay's own records.
⚠ Every one of these endpoints is merchant-API-key-authenticated — they're for your backend to call on the customer's behalf after you've authenticated them in your own system. Never expose your API key to a browser; proxy these calls through your own backend the same way you already do for POST /v1/payments.

A SubscriptionPlan is a versioned billing definition — amount, interval, trial, proration default — that a subscription is created onto or moved between. Editing a plan never rewrites subscriptions already on it; it creates a new version and archives the old one.

List subscription plans

GET /v1/subscription-plans → Try in Playground

Lists your brand's subscription plans, newest first.

Query parameters

ParameterTypeDescription
statusstringoptionalOne of active, archived.
product_idintegeroptionalFilter to plans attached to one product.

Returns 200 OK.

Create a subscription plan

POST /v1/subscription-plans → Try in Playground

Creates a versioned billing plan that subscriptions can be created or moved onto.

Body parameters

ParameterTypeDescription
product_idintegeroptionalAttach this plan to an existing product.
namestringrequiredPlan name (max 255 chars).
descriptionstringoptionalPlan description (max 1000 chars).
amountnumberrequiredBilling amount as a decimal (e.g. 29.99).
currencystringoptionalOne of GEL, USD, EUR, GBP. Defaults to GEL.
billing_interval_unitstringrequiredOne of day, week, month, year.
billing_interval_countintegerrequiredHow many of billing_interval_unit between charges (e.g. 3 + month = quarterly).
billing_anchor_dayintegeroptionalDay of month (1–28) charges anchor to, for month/year intervals.
trial_daysintegeroptionalFree trial length in days.
prorate_first_periodbooleanoptionalShorten the first charge so renewals land on billing_anchor_day. Unrelated to plan-change proration below.
proration_modestringoptionalDefault proration mode for changes onto/off this plan — one of immediate, deferred, none. Defaults to none.
is_mit_notice_exemptbooleanoptionalNBG trial/upcoming-charge compliance notice exemption for utilities/telecom/insurance/existing-debt merchants. Do not set this unless your business genuinely qualifies.
// 201 Created
{
  "uuid": "018f...",
  "product_id": null,
  "name": "Pro monthly",
  "amount": 29.99,
  "currency": "GEL",
  "billing_interval_unit": "month",
  "billing_interval_count": 1,
  "status": "active",
  "version": 1
}

Get a subscription plan

GET /v1/subscription-plans/{uuid} → Try in Playground

Returns a single subscription plan by UUID.

Path parameters

ParameterTypeDescription
uuidstringrequiredPlan UUID.

Returns 200 OK.

Edit a subscription plan

PATCH /v1/subscription-plans/{uuid} → Try in Playground

"Editing" a plan never mutates it in place — it creates a new version (a fresh UUID, version incremented) and archives this one, so subscriptions already on it keep their own billing snapshot untouched. Send only the fields you want to change; the rest carry over from the current version.

Path parameters

ParameterTypeDescription
uuidstringrequiredPlan UUID.

Body parameters

ParameterTypeDescription
namestringoptionalPlan name.
descriptionstringoptionalPlan description.
amountnumberoptionalBilling amount.
currencystringoptionalOne of GEL, USD, EUR, GBP.
billing_interval_unitstringoptionalOne of day, week, month, year.
billing_interval_countintegeroptionalBilling interval count.
billing_anchor_dayintegeroptionalDay of month (1–28) charges anchor to.
trial_daysintegeroptionalFree trial length in days.
prorate_first_periodbooleanoptionalFirst-period proration flag.
proration_modestringoptionalOne of immediate, deferred, none.
is_mit_notice_exemptbooleanoptionalNBG compliance notice exemption flag.

Returns 201 Created.

✓ Returns 422 subscription_plan_archived for an already-archived plan — create a new plan instead.

Archive a subscription plan

DELETE /v1/subscription-plans/{uuid} → Try in Playground

Archives a plan — it stops appearing as a choice for new subscriptions or plan changes, but existing subscriptions on it are untouched (they keep their own billing snapshot). Never actually deletes the row.

Path parameters

ParameterTypeDescription
uuidstringrequiredPlan UUID.

Returns 200 OK.

✓ Returns 422 subscription_plan_archived for a plan that is already archived.

A CustomerPaymentMethod is a reusable, vaulted saved card — decoupled from any one subscription, so a customer can hold several and reuse one across subscriptions. Adding one reuses the same zero-amount tokenization mechanism as Zero-Amount Tokenization.

List a customer's payment methods

GET /v1/customers/{id}/payment-methods → Try in Playground

Lists the saved, reusable payment methods on file for one customer.

Path parameters

ParameterTypeDescription
idintegerrequiredCustomer ID.

Query parameters

ParameterTypeDescription
statusstringoptionalOne of active, expired, failed, removed. Defaults to everything except removed.
// 200 OK
{
  "data": [
    {
      "uuid": "018f...",
      "customer_id": 42,
      "gateway_slug": "bog_card_subscriptions",
      "card_brand": "visa",
      "card_last4": "4242",
      "card_exp_month": 12,
      "card_exp_year": 2029,
      "is_default": true,
      "status": "active",
      "created_at": "2026-05-16T10:20:00Z"
    }
  ]
}
✓ card_brand/card_last4/card_exp_month/card_exp_year are not populated by every gateway — several *_subscriptions gateways only ever hand back an opaque recharge token, never card details.

Add a payment method

POST /v1/customers/{id}/payment-methods → Try in Playground

Starts a card-save flow for a customer — reuses the same zero-amount tokenize_only mechanism as Create a payment, just scoped to a customer instead of a subscription. Redirect the customer to the returned payment_url to complete it on the gateway's own hosted page; the new CustomerPaymentMethod appears once they finish.

Path parameters

ParameterTypeDescription
idintegerrequiredCustomer ID. Must have an email or phone on file — the finished payment method links back to this customer by matching one of those.

Body parameters

ParameterTypeDescription
gateway_slugstringrequiredA *_subscriptions gateway your brand has active (not paypal_subscriptions).
return_urlstringoptionalRedirect URL after the flow completes.
// 201 Created
{
  "uuid": "018f...",
  "payment_url": "https://qpy.ge/pay/018f...",
  "status": "pending"
}
✓ Returns 422 tokenize_only_gateway_unsupported for a non-*_subscriptions or paypal_subscriptions gateway_slug, 422 zero_amount_not_supported for a gateway that has not confirmed zero-amount card saves, and 422 validation_failed if the customer has neither email nor phone on file.

Remove a payment method

DELETE /v1/payment-methods/{uuid} → Try in Playground

Soft-removes a saved payment method (status becomes removed) — never a hard delete, so it stays visible on past subscription/event history.

Path parameters

ParameterTypeDescription
uuidstringrequiredPayment method UUID.

Returns 200 OK.

✓ Returns 422 payment_method_in_use if an active, past-due, paused, or pending-cancellation subscription is still charging this payment method — update or cancel that subscription first.

Set a payment method as default

POST /v1/payment-methods/{uuid}/set-default → Try in Playground

Marks one payment method as the default for its customer — scoped per customer + brand + gateway, since a payment method for one gateway can never recharge a subscription on another.

Path parameters

ParameterTypeDescription
uuidstringrequiredPayment method UUID.

Returns 200 OK.

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 · subscription.created · subscription.paused · subscription.resumed · subscription.plan_changed · subscription.payment_method_updated · subscription.trial_ending · subscription.upcoming_charge — charged/failed fire from POST /v1/payments/charge and the automatic renewal cycle; the rest fire from the Subscriptions management endpoints and the compliance notice scheduler.

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', 400

Error 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_codeHTTPWhen it occursWhat to do
missing_api_key401No Authorization headerAdd an Authorization: Bearer qpk_live_... header
invalid_api_key401Key does not exist or was revokedCheck your API key in Dashboard → API Keys
account_suspended403Merchant account is suspendedContact Quickpay support
test_key_live_mode403Test key used on a live-mode brandUse your live key or enable test mode on the brand
live_key_test_mode403Live key used on a test-mode brandUse your test key or switch the brand to live mode
no_gateway_license403No active module subscription for this gatewaySubscribe to the gateway module in Dashboard → Marketplace
idempotency_conflict409Same Idempotency-Key with a different amount or currencyUse a new key, or keep amount/currency unchanged on retry
not_found404Resource does not existCheck the UUID or slug
validation_failed422Request payload failed validationInspect the errors object and correct the offending fields
rate_limit_exceeded429More than 100 req/min per keyRetry after the Retry-After header interval
gateway_unavailable500Gateway API is temporarily downRetry with exponential back-off
coupon_not_found422Coupon code does not existCheck the coupon code value
coupon_inactive422Coupon is disabledContact merchant support
coupon_not_started422Coupon start date has not arrivedUse the coupon after its start date
coupon_expired422Coupon has expiredUse a different coupon code
coupon_product_mismatch422Coupon is only valid for a different productUse the correct product
coupon_below_min_amount422Order amount is below the coupon minimumIncrease the order amount
coupon_max_uses_reached422Coupon has been fully redeemedCoupon is no longer available
coupon_per_customer_limit422Customer has already used this couponUse a different coupon
coupon_code_taken422A coupon with this code already exists for the brandChoose a different, unused coupon code
line_items_mismatch422Sum of line_items totals ≠ amountFix line items so their totals match the payment amount
template_not_found422checkout_template_id does not existUse a valid ID from GET /v1/checkout-templates
select_options_required422select/radio field missing its options arrayAdd an options array (with label and value keys) to the field schema
amount_or_product_required422Neither amount nor product_uuid was providedInclude one of amount or product_uuid
refund_exceeds_remaining422Refund amount > remaining refundable amountCheck refunded_amount in the payment object
payment_not_refundable422Payment status does not allow refundsOnly paid and partially_refunded payments can be refunded
payment_not_cancellable422Payment status does not allow cancellationOnly pending, initiated, and transfer_pending payments can be cancelled
gateway_refund_unsupported422This gateway does not support API refundsIssue the refund manually via the bank's merchant portal
invoice_not_sendable422Invoice is paid or cancelledInvoices with status draft, sent, viewed, or overdue can be (re)sent
subscription_not_found404subscription_token does not exist or belongs to a different brandCheck the token from the original payment.paid webhook
subscription_inactive422Subscription is cancelled or has expiredOnly active subscriptions can be charged
subscription_already_cancelled422Subscription is already cancelledNo action needed — the subscription is already cancelled
charge_too_frequent429A charge was already initiated within the last 24 hoursRetry after the retry_after timestamp in the response body
webhook_not_configured422No webhook URL is configured for the brandSet a webhook URL with PUT /v1/webhooks before testing or rotating the secret
tokenize_only_gateway_unsupported422tokenize_only: true was sent with a gateway_slug that is not a *_subscriptions gateway, or is paypal_subscriptionsUse a supported *_subscriptions gateway slug (not paypal_subscriptions)
zero_amount_not_supported422The bank rejected the zero-amount card-save requestFall back to a small non-zero first charge for this gateway
subscription_plan_not_found404plan_uuid does not exist, is archived, or belongs to a different merchantUse an active plan uuid from GET /v1/subscription-plans
plan_currency_mismatch422The target plan is in a different currency than the subscription's current planPlan changes are same-currency only — create a plan in the matching currency
subscription_plan_archived422PATCH/DELETE was called on a plan that is already archivedCreate a new plan instead of editing an archived one
subscription_not_paused422POST .../resume was called on a subscription that is not pausedOnly a paused subscription can be resumed
subscription_reactivation_not_allowed422POST .../reactivate was called on an active, paused, or expired subscriptionOnly cancelled, past_due, or pending_cancellation subscriptions can be reactivated
paypal_subscription_action_unsupported422POST /v1/subscriptions, POST .../pause, POST .../resume, POST .../preview-plan-change, or POST .../change-plan targeted a paypal_subscriptions payment method/subscriptionPayPal manages its own billing schedule — create or manage the subscription through PayPal directly, not this endpoint. Cancel still works normally: POST .../cancel calls PayPal's own subscription-cancel API so billing actually stops.
customer_not_found404The customer id does not exist for this brandCheck the id from GET /v1/customers
payment_method_not_found404The payment method uuid does not exist for this brand/customerCheck the uuid from GET /v1/customers/{id}/payment-methods
payment_method_in_use422DELETE was called on a payment method still used by an active subscriptionUpdate or cancel the subscription using it first
no_active_payment_method422POST /v1/subscriptions was called for a customer with no saved payment methodCreate one first with POST /v1/customers/{id}/payment-methods