Integration
Key generation
Run these two commands in a terminal:
openssl genrsa -out finik_private.pem 2048
openssl rsa -in finik_private.pem -pubout > finik_public.pem| File | Purpose |
|---|---|
finik_private.pem | Sign every request with this key. Keep it secret because anyone who obtains it can send requests on your behalf. |
finik_public.pem | Send to Finik via a secure channel (email Finik) so that the service can check your requests. |
Signing requests
Each request requires a signature header - RSA-SHA256, Base64-encoded, generated with your private key. Finik provides ready-made packages for Node.js and Python.
Install @mancho.devs/authorizer and node-fetch, then:
import { Signer, RequestData } from '@mancho.devs/authorizer';
import fetch from 'node-fetch'; // or axios
// Choose the environment you call:
const baseUrl = 'https://api.acquiring.averspay.kg'; // prod
// const baseUrl = 'https://beta.api.acquiring.averspay.kg'; // beta
// IMPORTANT: Host header must match the URL host exactly.
const host = new URL(baseUrl).host;
// Your credentials
const apiKey = process.env.FINIK_API_KEY!; // from Finik
const privateKey = process.env.FINIK_PRIVATE_PEM!; // contents of finik_private.pem
const timestamp = Date.now().toString(); // UNIX ms
// Create Payment body per spec
const body = {
Amount: 100,
CardType: 'FINIK_QR',
PaymentId: '00000000-0000-0000-0000-000000000000', // use a real UUID
RedirectUrl: 'https://example.com/success',
Data: {
accountId: 'your-account-id',
name_en: 'your-qr-name',
},
};
// Build the canonical input for signing
const requestData: RequestData = {
httpMethod: 'POST',
path: '/v1/payment', // absolute path only (no query)
headers: {
Host: host, // must match baseUrl host
'x-api-key': apiKey, // included in signature
'x-api-timestamp': timestamp, // UNIX ms; same value used in signature
// You may send other headers, but only `host` and `x-api-*` are included in the signature.
},
queryStringParameters: undefined, // or { ... } if you have query; lib sorts & encodes
body, // plain JS object; lib will canonicalize/JSON-stringify
};
// Produce Base64 RSA-SHA256 signature
const signature = await new Signer(requestData).sign(privateKey);
// Send the actual HTTP request
const url = `${baseUrl}${requestData.path}`;
const res = await fetch(url, {
method: requestData.httpMethod,
headers: {
'content-type': 'application/json',
'x-api-key': apiKey,
'x-api-timestamp': timestamp,
signature, // <- attach signature header
},
body: JSON.stringify(body),
// If your API currently returns 302 (HTML) on S2S calls, prevent auto-follow:
redirect: 'manual', // remove once API returns 201 JSON by default
});
if (res.status === 302) {
// Read Location when you opt into redirects
console.log('Redirect to:', res.headers.get('location'));
} else {
console.error(res.status, await res.text());
}Only Host and headers starting with x-api- are included in the signature. Use the same timestamp value in both the
signature and the request header.
Create a payment
POST signed request to:
POST https://api.acquiring.averspay.kg/v1/payment — Production
POST https://beta.api.acquiring.averspay.kg/v1/payment — BetaHeaders
| Header | Description |
|---|---|
signature | The signature you generated. |
x-api-key | Your API key from Finik. |
x-api-timestamp | The current time in UNIX milliseconds is the same value that was used in the signature. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
Amount | Number | Optional | Fixed payment amount. If not specified, the client can pay any amount. |
CardType | String | Required | Always FINIK_QR. |
PaymentId | String | Required | Unique payment ID - prevents duplicate payments. |
RedirectUrl | String | Required | Where Finik sends the client after successful payment. |
Lang | String | Optional | Payment page language. Supported values: `ky`, `ru`, `en`. |
Data | Object | Required | Payment details - see below. |
Data object
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Required | Your Finik account ID - where the funds are credited. |
name_en | String | Required | The name of the QR code displayed to the client. |
webhookUrl | String | Required | Your server endpoint for payment status notifications. |
description | String | Optional | Description on the payment page. |
startDate | Number | Optional | Start of QR validity period (UNIX ms). |
endDate | Number | Optional | QR expiration date (UNIX ms). |
additionalData | Array | Optional | If the `value` field is not provided, the customer fills out the form before the payment methods are displayed. The maximum number of fields is 20. |
Multi-language pages
To select the payment page language, pass the Lang parameter at the top level of the request body. Supported values: ky, ru, en.
additionalData items
| Field | Type | Required | Description |
|---|---|---|---|
fieldId | String | Required | The key used for this field in the webhook payload. |
name | String | Required | Field label on the payment page. |
isHidden | Boolean | Optional | Hide the field on the payment page. |
value | String | Optional | Prefilled value. |
Example request body
{
"Amount": 100,
"CardType": "FINIK_QR",
"PaymentId": "a3f1c2e4-7b9d-4e2a-8c1f-3d0e9b2a5f6c",
"RedirectUrl": "https://example.com/success",
"Lang": "en",
"Data": {
"accountId": "your-account-id",
"name_en": "your-qr-name",
"webhookUrl": "https://merchant.example.com/webhooks/finik",
"description": "your-qr-description",
"startDate": 1737369000000,
"endDate": 1737455400000
}
}Response processing
A successful Create Payment returns a 302 redirect with the payment page URL in the Location header:
HTTP/1.1 302 Found
Location: https://qr.finik/<payment-path>- Call POST /v1/payment from the backend with redirects disabled.
- Read
Location- this is the URL of your payment page. - Send this URL to the client - redirect the browser to it or return it to your SPA / mobile application to open in webview.
- The client completes the payment and is redirected to your
RedirectUrl. - Finik sends a webhook with the final status - consider it the source of truth.
Disable automatic redirects and read the Location header
const res = await fetch("https://api.acquiring.averspay.kg/v1/payment", {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": apiKey, "x-api-timestamp": ts, signature },
body: JSON.stringify(body),
redirect: "manual", // don't auto-follow
});
if (res.status === 302) {
const paymentUrl = res.headers.get("location"); // send this to the browser
}The backend can redirect the browser (302 Location: <paymentUrl>) or return JSON to your SPA and set window.location = paymentUrl.
Error responses
Every error is returned as JSON in the following shape:
{
"StatusCode": 400,
"ErrorMessage": "..."
}Request validation errors
| HTTP Status | Error Message | Cause |
|---|---|---|
400 Bad Request | A payload must be provided. | The request body is empty. |
400 Bad Request | An unrecognizable payload is provided. | The body is not valid JSON. |
400 Bad Request | Invalid payment ID is required. | `PaymentId` is missing or is not a string. |
400 Bad Request | Payment ID must be less than or equal to 36 characters long. | `PaymentId` exceeds 36 characters. |
400 Bad Request | An invalid amount is provided. Amount must be greater than 0. | `Amount` is 0 or negative. |
400 Bad Request | Invalid IP address is provided. | `CustomerIp` (or the auto-detected source IP) is not a valid IP address. |
400 Bad Request | Card type is required. | `CardType` is missing. |
400 Bad Request | An unrecognizable card type is provided. | `CardType` is not currently supported (only `FINIK_QR` is supported at this time). |
400 Bad Request | RedirectUrl is required. | `CardType` is `FINIK_QR` and `RedirectUrl` is missing. |
400 Bad Request | Currency must be provided. | `Currency` is missing and no default could be inferred. |
400 Bad Request | An invalid currency is provided. | `Currency` is not `KGS` or `RUB`. |
400 Bad Request | "FINIK_QR" payment method currently does not support {Currency} currency. | The provided `Currency` is not supported for Finik QR (only `KGS` is supported). |
Payment data errors
These apply to the Data field of the request body.
| HTTP Status | Error Message | Cause |
|---|---|---|
400 Bad Request | Payment data must be provided. | `Data` is missing. |
400 Bad Request | An invalid payment data is provided. | `Data` could not be parsed into an object. |
400 Bad Request | Account ID is required in the payment data. | `Data.accountId` is missing. |
400 Bad Request | Item name is required in the payment data. | `Data.name_en` is missing. |
400 Bad Request | Start Date invalid in the payment data. | `Data.startDate` is present but is not a valid timestamp. |
400 Bad Request | End Date invalid in the payment data. | `Data.endDate` is present but is not a valid timestamp. |
400 Bad Request | Images in payment request is invalid: {images} | `Data.images` is present but is not an array. |
400 Bad Request | base64Image in payment request includes invalid base64 | An entry in Data.images is not a valid base64 image data URI (must match data:image/<type>;base64,<data>). |
400 Bad Request | Unable to create media files | The acquirer rejected or failed the image upload step. |
400 Bad Request | additionalData invalid in the payment data. | `Data.additionalData` is present but is not an array. |
400 Bad Request | additionalData exceeds maximum allowed size of 20. | More than 20 entries in `Data.additionalData`. |
400 Bad Request | "fieldId" not provided in the payment data. | An `additionalData` entry is missing `fieldId`. |
400 Bad Request | "name" not provided in the payment data. | An `additionalData` entry is missing `name`. |
Upstream and server errors
| HTTP Status | Error Message | Cause |
|---|---|---|
502 Bad Gateway | (relayed from the acquirer, or "Unable to process the payment request.") | |
400 Bad Request | (relayed from the acquirer, or "Unable to process the payment request.") | The HTTP call to the acquirer failed and the acquirer's own HTTP status was 400. The message is relayed the same way as above. |
These validation errors can also occur while processing the request:
| Error | Message | Cause |
|---|---|---|
ValidationException | A maximum of 10 media files can be uploaded. | Too many `mediaFiles`. |
ValidationException | The {media file id} is required. | Missing `mediaFiles` entry `id`. |
ValidationException | A duplicate ID in MediaFiles is provided. | Duplicate `mediaFiles` `id`. |
ValidationException | MediaFiles field contains an id that does not exists. | Unknown media `id`. |
ValidationException | The account.id is required. | Missing `account.id`. |
ValidationException | Account or parentId fields should be provided. | Missing `account` and `parentId`. |
ValidationException | The name_en is required. | Missing `name_en`. |
ValidationException | The requestId is required. | Missing `requestId`. |
ValidationException | The fixedAmount must be a decimal number with no more than 2 digits after the decimal point. | Invalid `fixedAmount` precision. |
ValidationException | The fixedAmount must be a greater than or equal to 0.01. | `fixedAmount` too low. |
ValidationException | (message defined by the merchant category code validator) | Invalid `merchantCategoryCode`. |
ValidationException | The requiredField.fieldId is required. | Missing `requiredFields` entry `fieldId`. |
ValidationException | Missing values for fields: {missing} | Missing required field values. |
ValidationException | Account is not provided. | No account available. |
ValidationException | Account status must be "Enabled" | Account is not enabled. |
Want to try it without writing code? Use the interactive Playground to send a test Create Payment request and complete a test payment.