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
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:
- Retrieve the
X-Infinia-Signature
header from the incoming request. - Compute the HMAC-SHA256 of the raw body using your webhook secret key.
- Encode the result in base64.
- Compare your computed signature with the
X-Infinia-Signature
header using a time-safe comparison. - 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
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
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 inERROR
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"
}
}