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