Finik.kg

Finik.kg

@razvrabotchik

Finik Web SDK Documentation

Introduction to the documentation

  1. This documentation helps you integrate Finik Payment System into your program so that your clients can make payments easily using any financial application in Kyrgyzstan.
  2. Primarily you will be dealing with Finik Acquiring Service that serves as payment provider for your application.
  3. In order to work with the Finik Acquiring Service you need to:
  4. Obtain API Key and your corporate account ID from Finik representatives
  5. Generate Private & Public Keys for the first step of your authorization into Finik Acquiring API
  6. Set signature generation process for the second step of your authorization into Finik Acquiring API
  7. Learn how to send requests to Finik Acquiring API
  8. Learn how to handle responses from Finik Acquiring API

Check the post below for a step-by-step demo of the payment flow in practice.

https://www.instagram.com/reel/DLXMWLjILKq/?igsh=MXg2bTdzZThhYms5bA

Prerequisites

This document describes the interface to interact with Finik Payment System through Finik Web SDK.

In order to successfully work with the Finik Web SDK you need to:

  1. Obtain the API Key from Finik representatives.
  2. Obtain your corporate account ID from Finik representatives. In some cases companies may have more than one account for different purposes.

Generate SSL Keys

Before proceeding to service integration, generate private key and public key certificates for your company. Use the following command to generate the keys on your command line terminal.

openssl genrsa -out finik_private.pem 2048

openssl rsa -in finik_private.pem -pubout > finik_public.pem
  • Private key:
  • Sign each request to Finik Acquiring API using this private key.
  • Important: Keep it secret. If someone gains access to this private key, they may send requests on your behalf.
  • Public key:
  • Send this public key to Finik representatives via a secure channel (Finik email) in order for the service to verify your requests.

Authorization via Signature

In Finik Acquiring Service each request is validated using the signature provided in the request HTTP header. In order to send a request it is necessary to generate signature using the private key.


For Node.js and Python there are available packages that provide convenient classes to generate signatures. We will soon provide similar libraries for other programming languages. In the meantime, for other languages use the signature algorithm explained at the end of this document.

Node.js

In Node.js you can use @mancho.devs/authorizer NPM package.

Below is the example of using package above.

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',
    merchantCategoryCode: '0742',
    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());
}

Python

In Python you can use mancho-devs/python-authorizer package (you can get all the information in README section)

Below is the example of using package above.

from authorizer import Signer
import json
import os
import time
import requests
from urllib.parse import urljoin

# Choose environment
BASE_URL = "https://api.acquiring.averspay.kg"        # prod
# BASE_URL = "https://beta.api.acquiring.averspay.kg"  # beta

HOST = "api.acquiring.averspay.kg"                     # must match BASE_URL host exactly
# HOST = "beta.api.acquiring.averspay.kg"

API_KEY = os.environ["FINIK_API_KEY"]                  # from Finik
PRIVATE_KEY_PEM = os.environ["FINIK_PRIVATE_PEM"]      # contents of finik_private.pem

timestamp = str(int(time.time() * 1000))               # UNIX ms

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",
        "merchantCategoryCode": "0742",
        "name_en": "your-qr-name"
    }
}

request_data = {
    "http_method": "POST",
    "path": "/v1/payment",
    "headers": {
        "Host": HOST,                     # must equal BASE_URL's host
        "x-api-key": API_KEY,
        "x-api-timestamp": timestamp,
    },
    # If you have queries, set a dict here; signer will encode/sort them
    "query_string_parameters": None,
    "body": body,                         # plain dict; signer will canonicalize/JSON-stringify
}

# Produce Base64 RSA-SHA256 signature
signature = Signer(**request_data).sign(PRIVATE_KEY_PEM)

# Send the request
url = urljoin(BASE_URL, request_data["path"])
resp = requests.post(
    url,
    headers={
        "content-type": "application/json",
        "x-api-key": API_KEY,
        "x-api-timestamp": timestamp,
        "signature": signature,
    },
    data=json.dumps(body, separators=(",", ":")),  # compact JSON (no spaces)
    # prevent auto-follow to get payment link in the header.location:
    allow_redirects=False,
)

if resp.status_code == 201:
    print("Created:", resp.json())  # -> { paymentId, paymentUrl, status }
elif resp.status_code in (301, 302, 303, 307, 308):
    print("Redirect to:", resp.headers.get("Location"))
else:
    print(resp.status_code, resp.text)

Other languages

1) Collect data required for generate signature (pseudocode).

1) data  = Lowercase(HTTP method) + "\n"
2) data += URIAbsolutePath + "\n"
3) data += ( header["Host"] and headers that start with x-api-*) + "\n"
4) data += queryStringParams + "\n" // Note: Don't add "\n" to data if there are no queryStringParams
5) data += json(request.body)

Notes:

  1. Convert the HTTP header to a lower case. For example: post, get.
  2. Append the path part of the URL that starts with a leading / (forward slash) and ends before any query parameter (excluding the ?).
  3. Append all headers in the following way:
  4. Take header [“Host”].
  5. Take all headers that start with x-api-*.
  6. Take all headers that start with x-api-*.
  7. Sort by header names alphabetically.
  8. Then concatenate in with following way:
parts  = "host" + ":" + String(header["Host"]) + "&"

parts += Lowercase(<HeaderName1>) + ":" + String(<value>) + "&"
parts += Lowercase(<HeaderName2>) + ":" + String(<value>) + "&"
...
parts += Lowercase(<HeaderNameN>) + ":" + String(<value>) 
  1. Next, query string parameters must be collected in the following way:
  2. Sort by query parameter names alphabetically
  3. Then concatenate in the following way:
parts  = URiEncode(<QueryParameter1>) + "=" + URiEncode(<value>) + "&"
parts += URiEncode(<QueryParameter2>) + "=" + URiEncode(<value>) + "&"
...
parts += URiEncode(<QueryParameterN>) + "=" + URiEncode(<value>)

If the query parameter value is empty, use an empty string as a value. For example, in http://mancho.dev/examplebucket?acl the acl query parameter has no value. Then, the concatenation should be as follows:

parts += URiEncode("acl") + "=" + "" 
  1. Finally, you should sort JSON object payload as well by object keys, then stringify it with keys sorted.

2) Once data is collected for signature, use the generated private key above and proceed to signature.


For example, in Java:

public String sign(String payload, String privatePath) {
  try{
    Signature signature = Signature.getInstance("SHA256withRSA");
    
    String privateKeyFile = new String(Files.readAllBytes(Paths.get(privatePath)));
    
    RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(privateKeyFile);
    
    PrivateKey privateKey = rsaKey.toPrivateKey();
    
    signature.initSign(privateKey);
    
    signature.update(payload.getBytes(StandardCharsets.UTF_8));
    
    byte[] signatureValue = signature.sign();
    
    return Base64.encodeBase64String(signatureValue);
  }catch (Exception e){
    throw new AppException(e.getMessage());
  }
}

Create Payment

The signature is required for the request.


Request type: POST

Request URL:


Headers: signature: <signature>

x-api-key: <apiKey>

x-api-timestamp: <timestamp>

HTTP header parameters

NameTypeRequired/OptionalDescriptionsignatureStringRequiredAuthorization signature generated by you.x-api-keyStringRequiredAPI client key; provided by Finik. Must also be provided in the signature generation step.x-api-timestampStringRequiredCurrent timestamp in milliseconds in UNIX type. The same timestamp must also be provided in the signature generation step.Request body parameters

NameTypeRequired/OptionalDescriptionAmountNumberRequiredA QR is generated with this specific amount. The merchant's client will not be able to specify any custom amount.CardTypeStringRequiredThe value must be FINIK_QR.PaymentIdStringRequiredA required field to control the uniqueness of a request. For each request it must be unique so that Finik makes sure there are no dublicate Payment being created for the current client.RedirectUrlStringRequiredOn success payment Finik Acquiring API will redirect you to this url.Data: {

accountId: string,

merchantCategoryCode: string,

name_en: string, webhookUrl: string

}Json ObjectRequiredVariables that must be specified.


- accountId - a required field. It is the Finik account where the funds will be deposited acquired from merchant's clients. Reach out to Finik representatives to receive your corporate accountId along with x-api-key.


- merchantCategoryCode - a required field used as mcc.


- name_en - a required field used as a QR name that will be displayed to merchant's clients on their devices upon payment.

- webhookUrl - a server to server payment status notifier apiSample request:

{ 
    "Amount": 100,
    "CardType": "FINIK_QR",
    "PaymentId": uuid(),
    "RedirectUrl": "https://example.com/success",
    "Data": `{
        "accountId": "your-account-id",
        "merchantCategoryCode": "0742",
        "name_en": "your-qr-name",
        "webhookUrl": "your-payment-status-webhook-api",
        
    }`
}

Response behavior (302 redirect) — what happens and how to integrate

When you Create Payment, Finik returns:

HTTP/1.1 302 Found
Location: https://qr.finik/<payment-path>



This is by design. The URL in Location is your payment page.

Many HTTP clients auto-follow redirects. If they follow this 302, they’ll fetch the HTML of the payment page, which is meant for the browser, not your backend. That’s why you see HTML.

What you should do instead:

  1. Call POST /v1/payment (signed) from your backend.
  2. Do not follow the redirect. Read the Location header.
  3. Send that URL to the user (either: 302 redirect the browser to it, or return it to your SPA/mobile app to open in a webview).
  4. The user completes payment on the Finik page.
  5. Finik immediately 302-redirects the user to your RedirectUrl.
  6. Separately, Finik sends a server-to-server webhook with the final status. Treat the webhook as source of truth.

Backend code: prevent auto-follow + read Location

  • Node (fetch):
CopyEdit
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
}



  • Node (axios):
const res = await axios.post(url, body, {
  headers: { "x-api-key": apiKey, "x-api-timestamp": ts, signature },
  maxRedirects: 0, // <-- don't auto-follow
validateStatus: s => [201, 302].includes(s) || s >= 400
});const paymentUrl = res.headers.location;
  • Python (requests):
resp = requests.post(url, headers=hdrs, json=body, allow_redirects=False)  # <-- don't auto-follow
if resp.status_code == 302:
    payment_url = resp.headers["Location"]
  • curl (manual by default):
curl -i -X POST https://api.acquiring.averspay.kg/v1/payment \
  -H 'x-api-key: ...' -H 'x-api-timestamp: ...' -H 'signature: ...' \
  -H 'content-type: application/json' \
  --data '{"Amount":100,"CardType":"FINIK_QR", ... }'
# Look at the Location header in the 302 response

Then your backend can either:
  • Redirect the browser: 302 Location: <paymentUrl>
  • Return JSON to your SPA: { "paymentUrl": "<paymentUrl>" } and the frontend window.location = paymentUrl.

Quick troubleshooting

  • Seeing HTML instead of JSON? Your HTTP client auto-followed the 302. Disable redirects (see code above) and read Location.


Sample response to an erroneous request:

{ 
    "statusCode": 400,
    "ErrorMessage": "A valid UNIX timestamp must be provided in the header"
}
{ 
    "statusCode": 400,
    "ErrorMessage": "An invalid amount is provided. Amount must be greater than 0."
}
{ 
    "statusCode": 401,
    "ErrorMessage": "An invalid signature is provided"
} 
{ 
    "statusCode": 403,
    "ErrorMessage": "Forbidden"
} 

Payments Status Webhook (Callback)

After each completed payment, Finik sends a POST to your webhook URL. Verify the request using the same signature algorithm as Create Payment, then finalize the order based on status.

Inbound request example

makefile
CopyEdit
POST /webhooks/finik HTTP/1.1Host: merchant.example.com
Content-Type: application/json
x-api-timestamp: 1737369012345
signature: Gk3GqQ1O0z+4wC3…(Base64)…u1U=

{
  "id": "transaction-id-15423_CREDIT",
  "transactionId": "transaction-id-241234",
  "status": "SUCCEEDED",            // or "FAILED"
"amount": 100,
  "net": 100,
  "accountId": "your account id",
  "fields": {
    "amount": 100,
    "fieldId1": "value1",
    "fieldId2": "value2"
  },
  "requestDate": 1737369012345,
  "transactionDate": 1737369012345,
  "transactionType": "DEBIT",
  "receiptNumber": "some-number"
}



Additional x-api-* headers may be present; they must be included in the canonical string exactly like in Create Payment.

How to handle (reuse your existing signer)

  1. Build canonical string: feed the incoming request’s method, path, headers (use host + all x-api-*), query, and body to your existing createSignature/canonicalization function (the same one you use before signing Create Payment).
  2. Verify: compare the Base64 signature header against that canonical string using the Finik public key for your environment (Prod/Beta).
  3. Check timestamp: ensure x-api-timestamp is within your allowed skew (e.g., ±5 min).
  4. Process idempotently: use transactionId (or id) to de-dupe; update order by status.
  5. Ack fast: respond 200 OK quickly; do heavy work async.

Quick troubleshooting

  • Invalid signature → wrong Host, missing an x-api-* header in canonical string, or body keys not sorted exactly like your Create Payment logic.
  • Multiple callbacks → design for at-least-once delivery; de-dupe by transactionId.



Prod public key (if you are working with production environment use the following key):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuF/PUmhMPPidcMxhZBPb
BSGJoSphmCI+h6ru8fG8guAlcPMVlhs+ThTjw2LHABvciwtpj51ebJ4EqhlySPyT
hqSfXI6Jp5dPGJNDguxfocohaz98wvT+WAF86DEglZ8dEsfoumojFUy5sTOBdHEu
g94B4BbrJvjmBa1YIx9Azse4HFlWhzZoYPgyQpArhokeHOHIN2QFzJqeriANO+wV
aUMta2AhRVZHbfyJ36XPhGO6A5FYQWgjzkI65cxZs5LaNFmRx6pjnhjIeVKKgF99
4OoYCzhuR9QmWkPl7tL4Kd68qa/xHLz0Psnuhm0CStWOYUu3J7ZpzRK8GoEXRcr8
tQIDAQAB
-----END PUBLIC KEY-----

Beta public key (if you are working with beta environment use the following key):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlrlKz/8gLWd1ARWGA/8
o3a3Qy8G+hPifyqiPosiTY6nCHovANMIJXk6DH4qAqqZeLu8pLGxudkPbv8dSyG7
F9PZEAryMPzjoB/9P/F6g0W46K/FHDtwTM3YIVvstbEbL19m8yddv/xCT9JPPJTb
LsSTVZq5zCqvKzpupwlGS3Q3oPyLAYe+ZUn4Bx2J1WQrBu3b08fNaR3E8pAkCK27
JqFnP0eFfa817VCtyVKcFHb5ij/D0eUP519Qr/pgn+gsoG63W4pPHN/pKwQUUiAy
uLSHqL5S2yu1dffyMcMVi9E/Q2HCTcez5OvOllgOtkNYHSv9pnrMRuws3u87+hNT
ZwIDAQAB
-----END PUBLIC KEY-----


Report Page