Webhooks

Handling webhook

After each completed payment, Finik sends a POST to your webhookUrl. Check it, then update your order.

Example payload

POST /webhooks/finik
{
  "id": "transaction-id-15423_DEBIT", // Finik operation ID (the _DEBIT suffix means a debit)
  "transactionId": "transaction-id-241234", // payment transaction ID — use it for idempotency (deduplication)
  "status": "success", // payment status; may arrive in any case — check case-insensitively
  "amount": 100, // payment amount
  "fields": { // payment form fields and your additionalData
    "amount": 100, // amount captured on the form
    "paymentId": "payment-id-241234", // the PaymentId you passed to POST /v1/payment
    "fieldId1": "value1", // value of your additionalData field with fieldId "fieldId1"
    "fieldId2": "value2" // value of your additionalData field with fieldId "fieldId2"
  },
  "requestDate": 1737369012345, // request creation time (UNIX, ms)
  "transactionDate": 1737369012345, // transaction processing time (UNIX, ms)
  "service": { "id": "VISA" } // present only if card acceptance (Visa) is enabled for you
}

What to do

  1. Verify the signature - Build a canonical string from the incoming request (method, path, host + all headers x-api-*, query, body), then verify the signature header against the Finik public key for your environment.
  2. Check the timestamp - Make sure x-api-timestamp is within the valid window (eg ±5 minutes).
  3. Ensure idempotency - Use transactionId (or id) for deduplication. Finik can deliver one webhook several times (“at least once” delivery).
  4. Respond quickly - Return 200 OK right away and do the heavy lifting asynchronously.

Delivery retries

Webhooks are delivered "at least once". If your server does not respond with 200 OK (or responds with an error), Finik retries the delivery.

  • First 5 attempts — every 30 seconds.
  • After that — the interval doubles (exponential backoff): it starts at 60 seconds and is capped at 24 hours (86,400 seconds).
AttemptDelay
060 sec (1 min)
1120 sec (2 min)
2240 sec (4 min)
3480 sec (8 min)
4960 sec (16 min)
51920 sec (32 min)
63840 sec (1 h 4 min)
77680 sec (2 h 8 min)
815360 sec (4 h 16 min)
930720 sec (8 h 32 min)
1061440 sec (17 h 4 min)
11122880 sec → capped to 86400 sec (24 h)

Once the delay reaches 24 hours it stops growing — all further attempts are made once a day.

How to verify the signature

Below are examples of how to correctly verify the signature.

Install @mancho.devs/authorizer and node-fetch, then:

check-signature.ts
import { Signer } from "@mancho.devs/authorizer";
import * as dotenv from "dotenv";
dotenv.config({ path: ".env.development" });

const publicKey = process.env.FINIK_BETA_PUBLIC_KEY;

const requestData = {
  body: {
    fields: {
      amount: 1,
      webhook_url: "https://huntpro.kg/finik-callback",
      paymentId: "141fc797-0366-480e-8ba7-2982632b2b2c",
      success_redirect_url:
        "https://api.acquiring.averspay.kg/v1/redirect?paymentId=141fc797-0366-480e-8ba7-2982632b2b2c&clientId=1295102413&status=succeeded",
      name: "HuntPro",
      qrTransactionId: "aaa76e79-f808-4ac5-b88e-76a9b4055832",
    },
    amount: 1,
    transactionDate: 1781697937466,
    clientId: "25c1aabe-fc4a-4968-85d9-90600466e9a5",
    id: "320259584_aaa76e79-f808-4ac5-b88e-76a9b4055832_DEBIT",
    status: "succeeded",
    transactionId: "141fc797-0366-480e-8ba7-2982632b2b2c",
    data: {
      accountId: "8d4e1d11-e50c-4278-a5e2-4e55de196c6a",
      description: "Order 2606",
      webhookUrl: "https://huntpro.kg/finik-callback",
      name_en: "HuntPro",
    },
  },
  httpMethod: "POST",
  headers: {
    "Content-Type": "application/json",
    Host: "huntpro.kg",
    "x-api-timestamp": "1781763261255",
  },
  path: "/finik-callback",
  queryStringParameters: null,
};

const signature = "jS1a8HW4BTIea8iCmoW/EF7FXUA0JSsHe/kSRBfCYdxFwvyrMe0SOfU9pAKzOOO0bIsa9FW+WqAzqxExHjcscmG8k/vwgB+dCMcM04K91vTL6eZAVOE/Hcx2iALNZnG6UVnP/jnUt8MjK+xwBhamwFktxdUovf+cVFZ2io10RAT1pOEIHtZceXbQeg7XlhcPuTAKmOuF1B4gGoksRtGglGwwyvYaa9ml8OPMP3uoCaauYqT0aUKLTNsJdFfO0Q3Dvsy9uyWvuVa5t6adbIm+SHlbz9VGSZ2xrwgETUBVj86dfA3oiAgERLKhQTBSMTxRYlUKhPVHGfR3kjAhD3g4JQ==";

async function checkSignature() {
  const isValid = await new Signer(requestData as any).verify(
    publicKey!,
    signature,
  );

  console.log("Is valid signature:", isValid);
}

checkSignature();