Integration

Key generation

Run these two commands in a terminal:

terminal
openssl genrsa -out finik_private.pem 2048
openssl rsa -in finik_private.pem -pubout > finik_public.pem
FilePurpose
finik_private.pemSign every request with this key. Keep it secret because anyone who obtains it can send requests on your behalf.
finik_public.pemSend 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:

create-payment.ts
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:

API endpoints
POST https://api.acquiring.averspay.kg/v1/payment — Production
POST https://beta.api.acquiring.averspay.kg/v1/payment — Beta

Headers

HeaderDescription
signatureThe signature you generated.
x-api-keyYour API key from Finik.
x-api-timestampThe current time in UNIX milliseconds is the same value that was used in the signature.

Request body

FieldTypeRequiredDescription
AmountNumberOptionalFixed payment amount. If not specified, the client can pay any amount.
CardTypeStringRequiredAlways FINIK_QR.
PaymentIdStringRequiredUnique payment ID - prevents duplicate payments.
RedirectUrlStringRequiredWhere Finik sends the client after successful payment.
LangStringOptionalPayment page language. Supported values: `ky`, `ru`, `en`.
DataObjectRequiredPayment details - see below.

Data object

FieldTypeRequiredDescription
accountIdStringRequiredYour Finik account ID - where the funds are credited.
name_enStringRequiredThe name of the QR code displayed to the client.
webhookUrlStringRequiredYour server endpoint for payment status notifications.
descriptionStringOptionalDescription on the payment page.
startDateNumberOptionalStart of QR validity period (UNIX ms).
endDateNumberOptionalQR expiration date (UNIX ms).
additionalDataArrayOptionalIf 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

FieldTypeRequiredDescription
fieldIdStringRequiredThe key used for this field in the webhook payload.
nameStringRequiredField label on the payment page.
isHiddenBooleanOptionalHide the field on the payment page.
valueStringOptionalPrefilled value.

Example request body

POST /v1/payment — 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:

response
HTTP/1.1 302 Found
Location: https://qr.finik/<payment-path>
  1. Call POST /v1/payment from the backend with redirects disabled.
  2. Read Location - this is the URL of your payment page.
  3. Send this URL to the client - redirect the browser to it or return it to your SPA / mobile application to open in webview.
  4. The client completes the payment and is redirected to your RedirectUrl.
  5. Finik sends a webhook with the final status - consider it the source of truth.

Disable automatic redirects and read the Location header

fetch.js
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:

error response
{
  "StatusCode": 400,
  "ErrorMessage": "..."
}

Request validation errors

HTTP StatusError MessageCause
400 Bad RequestA payload must be provided.The request body is empty.
400 Bad RequestAn unrecognizable payload is provided.The body is not valid JSON.
400 Bad RequestInvalid payment ID is required.`PaymentId` is missing or is not a string.
400 Bad RequestPayment ID must be less than or equal to 36 characters long.`PaymentId` exceeds 36 characters.
400 Bad RequestAn invalid amount is provided. Amount must be greater than 0.`Amount` is 0 or negative.
400 Bad RequestInvalid IP address is provided.`CustomerIp` (or the auto-detected source IP) is not a valid IP address.
400 Bad RequestCard type is required.`CardType` is missing.
400 Bad RequestAn unrecognizable card type is provided.`CardType` is not currently supported (only `FINIK_QR` is supported at this time).
400 Bad RequestRedirectUrl is required.`CardType` is `FINIK_QR` and `RedirectUrl` is missing.
400 Bad RequestCurrency must be provided.`Currency` is missing and no default could be inferred.
400 Bad RequestAn 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 StatusError MessageCause
400 Bad RequestPayment data must be provided.`Data` is missing.
400 Bad RequestAn invalid payment data is provided.`Data` could not be parsed into an object.
400 Bad RequestAccount ID is required in the payment data.`Data.accountId` is missing.
400 Bad RequestItem name is required in the payment data.`Data.name_en` is missing.
400 Bad RequestStart Date invalid in the payment data.`Data.startDate` is present but is not a valid timestamp.
400 Bad RequestEnd Date invalid in the payment data.`Data.endDate` is present but is not a valid timestamp.
400 Bad RequestImages in payment request is invalid: {images}`Data.images` is present but is not an array.
400 Bad Requestbase64Image in payment request includes invalid base64An entry in Data.images is not a valid base64 image data URI (must match data:image/<type>;base64,<data>).
400 Bad RequestUnable to create media filesThe acquirer rejected or failed the image upload step.
400 Bad RequestadditionalData invalid in the payment data.`Data.additionalData` is present but is not an array.
400 Bad RequestadditionalData 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 StatusError MessageCause
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:

ErrorMessageCause
ValidationExceptionA maximum of 10 media files can be uploaded.Too many `mediaFiles`.
ValidationExceptionThe {media file id} is required.Missing `mediaFiles` entry `id`.
ValidationExceptionA duplicate ID in MediaFiles is provided.Duplicate `mediaFiles` `id`.
ValidationExceptionMediaFiles field contains an id that does not exists.Unknown media `id`.
ValidationExceptionThe account.id is required.Missing `account.id`.
ValidationExceptionAccount or parentId fields should be provided.Missing `account` and `parentId`.
ValidationExceptionThe name_en is required.Missing `name_en`.
ValidationExceptionThe requestId is required.Missing `requestId`.
ValidationExceptionThe fixedAmount must be a decimal number with no more than 2 digits after the decimal point.Invalid `fixedAmount` precision.
ValidationExceptionThe 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`.
ValidationExceptionThe requiredField.fieldId is required.Missing `requiredFields` entry `fieldId`.
ValidationExceptionMissing values for fields: {missing}Missing required field values.
ValidationExceptionAccount is not provided.No account available.
ValidationExceptionAccount 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.