📩 Webhooks

Webhooks allow your application to receive real-time notifications whenever specific events occur, without the need to continuously poll our API.

When a subscribed event happens (for example: a payment is confirmed, a subscription is canceled, or an item’s status changes), our server sends an HTTP POST request to the callback URL you have configured.

Each webhook request contains a JSON payload describing the event and HTTP headers that help you verify authenticity and process messages safely.


Security: X-Infinia-Signature

Every webhook request includes an X-Infinia-Signature HTTP header.
This value is an HMAC (Hash-Based Message Authentication Code) generated using:

  • The raw webhook payload (exact request body)
  • Your shared secret key (provided when you configure the webhook)
  • HMAC-SHA256 as the hashing algorithm

How to verify:

  1. Retrieve the X-Infinia-Signature header from the incoming request.
  2. Compute the HMAC-SHA256 of the raw body using your webhook secret key.
  3. Compare your computed signature with the X-Infinia-Signature header using a time-safe comparison.
  4. If they match, the request is authentic and unmodified.

import hmac
import hashlib

def is_valid_signature(raw_body: str, signature_header: str, client_id: str) -> bool:
    computed_hmac = hmac.new(
        key=client_id.encode('utf-8'),
        msg=raw_body.encode('utf-8),
        digestmod=hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(computed_hmac, signature_header)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class WebhookVerifier {
    public static boolean isValidSignature(String rawBody, String signatureHeader, String clientId) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(clientId.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKeySpec);

        byte[] hmacBytes = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : hmacBytes) {
            sb.append(String.format("%02x", b));
        }
        String computedHmac = sb.toString();

        return computedHmac.equalsIgnoreCase(signatureHeader);
    }
}
const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, clientId) {
  const hmac = crypto.createHmac('sha256', clientId)
                     .update(rawBody, 'utf8')
                     .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(hmac, 'utf8'),
    Buffer.from(signatureHeader, 'utf8')
  );
}
<?php
function isValidSignature(string $rawBody, string $signatureHeader, string $clientId): bool {
    $computedHmac = hash_hmac('sha256', $rawBody, $clientId);
    return hash_equals($computedHmac, $signatureHeader);
}
?>
#include <iostream>
#include <string>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <iomanip>
#include <sstream>

// Converts bytes to hex string
std::string to_hex(const unsigned char* data, size_t length) {
    std::ostringstream oss;
    for (size_t i = 0; i < length; ++i)
        oss << std::hex << std::setw(2) << std::setfill('0') << (int)data[i];
    return oss.str();
}

// Time-safe comparison
bool timing_safe_compare(const std::string& a, const std::string& b) {
    if (a.size() != b.size()) return false;
    volatile unsigned char diff = 0;
    for (size_t i = 0; i < a.size(); ++i)
        diff |= a[i] ^ b[i];
    return diff == 0;
}

bool isValidSignature(const std::string& rawBody, const std::string& signatureHeader, const std::string& clientId) {
    unsigned char* result;
    unsigned int len = EVP_MAX_MD_SIZE;

    result = HMAC(EVP_sha256(),
                  clientId.data(), clientId.size(),
                  reinterpret_cast<const unsigned char*>(rawBody.data()), rawBody.size(),
                  nullptr, nullptr);

    std::string computedHmac = to_hex(result, 32); // SHA256 output is 32 bytes

    return timing_safe_compare(computedHmac, signatureHeader);
}


Idempotency: X-Idempotency-Key

To prevent processing the same event multiple times, each webhook request includes a unique X-Idempotency-Key header.

Key points:

  • The value is a UUID that uniquely identifies the delivery attempt for a specific event.
  • Store the keys of processed events for a safe period (e.g., 24 hours).
  • If the same X-Idempotency-Key is received again, skip re-processing and return a successful status code.

Event: event

The event header specifies the type of notification being delivered by the webhook.
It allows your system to distinguish between different kinds of events without having to parse the payload.

  • Examples:
    • movement
    • payment
    • payout

Retry Policy

If your server:

  • Does not respond with a 2xx HTTP status code, or experiences a network error then the webhook delivery will be retried.

Retry behavior:

  • A maximum of 4 retry attempts are made.
  • Each retry occurs every 15 minutes after the previous attempt.
  • If the 4th retry fails, the webhook is marked with the status ERROR. Once in ERROR status, no further automatic retries will be performed.

You may manually re-send failed webhooks through the dashboard or API.


Example Request

POST /webhooks/movements HTTP/1.1
Host: example.com
Content-Type: application/json
event: movement
X-Infinia-Signature: 5f2f77a1c3f12e7c9f81b2e6f2d4d9b8e0d9a1a4a2b4d8a6f0f1a9b8e0d3c1f0
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "id": "338131",
  "account_id": "640",
  "company_id": "12344",
  "amount": 10,
  "currency": "MXN",
  "country": "MX",
  "balance": 10,
  "description": "Initial deposit",
  "created_at": "2025-08-11T16:05:06.749462",
  "updated_at": "2025-08-11T16:05:06.749462",
  "third_party": null,
  "operation": null,
  "company": {
    "id": "12344",
    "name": "InfiniaCompany"
  }
}