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-SignatureEvery 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-Signatureheader 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-Signatureheader 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-KeyTo 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-Keyis received again, skip re-processing and return a successful status code.
Event: event
eventThe 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:
movementpaymentpayout
Retry Policy
If your server:
- Does not respond with a
2xxHTTP 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 inERRORstatus, 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"
}
}
