📩 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. Encode the result in base64.
  4. Compare your computed signature with the X-Infinia-Signature header using a time-safe comparison.
  5. If they match, the request is authentic and unmodified.

import hmac
import base64
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
    ).digest()

    provided_hmac = base64.b64decode(signature_header)
    return hmac.compare_digest(computed_hmac, provided_hmac)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

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

        byte[] computedHmacBytes = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));

        // Decode provided Base64 signature
        byte[] providedHmacBytes;
        try {
            providedHmacBytes = Base64.getDecoder().decode(signatureHeader);
        } catch (IllegalArgumentException e) {
            return false; // Invalid Base64 input
        }

        // Constant-time comparison
        if (computedHmacBytes.length != providedHmacBytes.length) {
            return false;
        }

        int result = 0;
        for (int i = 0; i < computedHmacBytes.length; i++) {
            result |= computedHmacBytes[i] ^ providedHmacBytes[i];
        }

        return result == 0;
    }
}
const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, clientId) {
  // Compute HMAC-SHA256 and get raw bytes
  const computedHmac = crypto
    .createHmac('sha256', clientId)
    .update(rawBody, 'utf8')
    .digest(); // returns a Buffer (raw bytes)

  let providedHmac;
  try {
    providedHmac = Buffer.from(signatureHeader, 'base64');
  } catch {
    return false; // invalid Base64 input
  }

  if (computedHmac.length !== providedHmac.length) {
    return false;
  }

  return crypto.timingSafeEqual(computedHmac, providedHmac);
}
<?php
function isValidSignature(string $rawBody, string $signatureHeader, string $clientId): bool {
    // Compute raw HMAC-SHA256 bytes
    $computedHmacRaw = hash_hmac('sha256', $rawBody, $clientId, true);

    // Encode to Base64 to match the signature format
    $computedHmacB64 = base64_encode($computedHmacRaw);

    // Use constant-time comparison for security
    return hash_equals($computedHmacB64, $signatureHeader);
}
?>
#include <iostream>
#include <string>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <cppcodec/base64_rfc4648.hpp> 

// 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[EVP_MAX_MD_SIZE];
    unsigned int len = 0;

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

    std::string computedHmac = cppcodec::base64_rfc4648::encode(result, len);

    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"
  }
}