Webhooks
TumiPay Card Payments sends webhooks to notify your system about important events in the payment lifecycle. Webhooks are sent asynchronously via HTTP POST requests with JSON payloads, allowing you to react to events in real-time without polling the API.
What are Webhooks? Webhooks are HTTP callbacks that notify your application when specific events occur. Instead of continuously checking for updates, TumiPay sends a POST request to your configured endpoint whenever a transaction or subscription status changes.
Overview
When you integrate Card Payment webhooks, your system receives automatic notifications for:
Transaction Events : Authorization, capture, and decline events
Subscription Events : Creation, cancellation, and expiration events
Each webhook includes:
A unique webhook identifier
An event type identifier
A timestamp
An idempotency key for duplicate detection
Event-specific data
An HMAC SHA256 signature for security verification
Webhook Configuration
Before receiving webhooks, you must configure your webhook endpoint. To configure webhooks for Card Payment, please contact the support team.
How to Configure Webhooks To set up webhook notifications, you need to:
Contact Support : Send an email to [email protected] or request assistance through the support channel
Provide Your Webhook URL : Your HTTPS endpoint where webhooks will be sent (must be publicly accessible)
Receive Secret Key : You will receive a shared secret key used to sign and verify webhook payloads
Activation : The support team will activate webhook delivery for your account
Webhook URL Your HTTPS endpoint where webhooks will be sent. Must be publicly accessible.
Secret Key A shared secret used to sign and verify webhook payloads. Keep this secure!
Status Active or inactive. Only one active webhook configuration per merchant is supported.
For webhook configuration assistance, contact:
Your webhook URL must use HTTPS. TumiPay will only send webhooks to secure endpoints.
Every webhook request includes specific HTTP headers that provide metadata and security information.
Content-Type : application/json
X-Webhook-Event : transaction.authorized
X-Webhook-Timestamp : 2024-01-01T10:00:00.000Z
X-Webhook-Id : 550e8400-e29b-41d4-a716-446655440000
X-Idempotency-Key : transaction.authorized:transaction-uuid-123
X-Webhook-Signature : c829a84733035c391f71cfeb73113a6b3161eac74f37e97c930bc44040c8e515
User-Agent : TumiPay-Webhooks/1.0
Signature Verification
All webhooks are signed using HMAC SHA256 to ensure authenticity and integrity. You must verify the signature before processing any webhook.
Always verify the webhook signature before processing. This ensures the webhook originated from TumiPay and has not been tampered with.
The signature is a hexadecimal string (64 characters) representing the HMAC SHA256 hash.
Example: c829a84733035c391f71cfeb73113a6b3161eac74f37e97c930bc44040c8e515
Verification Process
Extract the signature
Get the signature from the X-Webhook-Signature header (itβs a hexadecimal string)
Compute expected signature
Create HMAC SHA256 of the raw JSON payload using your secret key
Compare securely
Use constant-time comparison to compare signatures (prevents timing attacks)
Code Examples
function verifyWebhookSignature ( string $payload , string $signature , string $secretKey ) : bool
{
// Compute expected signature
$expectedSignature = hash_hmac ( 'sha256' , $payload , $secretKey );
// Constant-time comparison (prevents timing attacks)
return hash_equals ( $expectedSignature , $signature );
}
Always use constant-time comparison functions (hash_equals, timingSafeEqual, compare_digest) to prevent timing attacks. Never use simple string comparison (== or ===).
Event Types
TumiPay Card Payments sends the following webhook event types:
Transaction Events:
transaction.authorized - A transaction has been successfully authorized
transaction.captured - A transaction has been successfully captured
transaction.declined - A transaction has been declined
Subscription Events:
subscription.created - A subscription has been successfully created
subscription.cancelled - A subscription has been cancelled
subscription.expired - A subscription has expired
Payload Structure
All webhook payloads follow a consistent structure:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "transaction.authorized" ,
"timestamp" : "2024-01-01T10:00:00.000Z" ,
"idempotency_key" : "transaction.authorized:transaction-uuid-123" ,
"data" : {
// Event-specific data structure
}
}
Payload Fields:
Field Type Description idstring Unique identifier for this webhook (UUID) eventstring Event type identifier (e.g., transaction.authorized, subscription.created) timestampstring ISO 8601 timestamp in UTC format idempotency_keystring Key for duplicate detection (format: {event}:{entity_uuid}) dataobject Event-specific data structure containing transaction or subscription details
Event Payloads
This section provides complete JSON examples for each webhook event type. Each example shows the full payload structure youβll receive.
transaction.authorized
When itβs sent: When a transaction is successfully authorized (preauthorization completed).
Transaction Types:
PRE_AUTH_TRANSACTION - Initial pre-authorization
RENEWAL_PRE_AUTH_TRANSACTION - Renewal pre-authorization
Example - PRE_AUTH_TRANSACTION:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "transaction.authorized" ,
"timestamp" : "2024-01-01T10:00:00.000Z" ,
"idempotency_key" : "transaction.authorized:transaction-uuid-123" ,
"data" : {
"transaction" : {
"transaction_id" : "transaction-uuid-123" ,
"transaction_type" : "PRE_AUTH_TRANSACTION" ,
"transaction_status" : "APPROVED" ,
"amount" : "100.00" ,
"currency" : "COP" ,
"reference_id" : "merchant-reference-123" ,
"transaction_date" : "2024-01-01T10:00:00Z"
},
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "ACTIVE"
}
}
}
Example - RENEWAL_PRE_AUTH_TRANSACTION:
{
"id" : "550e8400-e29b-41d4-a716-446655440001" ,
"event" : "transaction.authorized" ,
"timestamp" : "2024-02-01T10:00:00.000Z" ,
"idempotency_key" : "transaction.authorized:transaction-uuid-124" ,
"data" : {
"transaction" : {
"transaction_id" : "transaction-uuid-124" ,
"transaction_type" : "RENEWAL_PRE_AUTH_TRANSACTION" ,
"transaction_status" : "APPROVED" ,
"amount" : "150.00" ,
"currency" : "COP" ,
"reference_id" : "merchant-reference-124" ,
"transaction_date" : "2024-02-01T10:00:00Z"
},
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "ACTIVE"
}
}
}
Note: The subscription object is optional and may not be present if the transaction is not associated with a subscription.
transaction.captured
When itβs sent: When a transaction is successfully captured (payment completed).
Example:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "transaction.captured" ,
"timestamp" : "2024-01-01T10:05:00.000Z" ,
"idempotency_key" : "transaction.captured:transaction-uuid-789" ,
"data" : {
"transaction" : {
"transaction_id" : "transaction-uuid-789" ,
"transaction_type" : "COMPLETION_TRANSACTION" ,
"transaction_status" : "APPROVED" ,
"amount" : "100.00" ,
"currency" : "COP" ,
"reference_id" : "merchant-reference-123" ,
"transaction_date" : "2024-01-01T10:05:00Z" ,
"linked_transaction_id" : "transaction-uuid-123"
},
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "ACTIVE"
}
}
}
Note: The linked_transaction_id field references the original authorization transaction that was captured. This field is only present when the completed/capture transaction was performed from a renewal preauthorization, so it will only be included in those cases.
transaction.declined
When itβs sent: When a transaction is declined by the payment provider.
Example:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "transaction.declined" ,
"timestamp" : "2024-01-01T10:00:00.000Z" ,
"idempotency_key" : "transaction.declined:transaction-uuid-123" ,
"data" : {
"transaction" : {
"transaction_id" : "transaction-uuid-123" ,
"transaction_type" : "PRE_AUTH_TRANSACTION" ,
"transaction_status" : "DECLINED" ,
"amount" : "100.00" ,
"currency" : "COP" ,
"reference_id" : "merchant-reference-123" ,
"transaction_date" : "2024-01-01T10:00:00Z"
},
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "FAILED"
}
}
}
subscription.created
When itβs sent: When a subscription is successfully created.
Example:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "subscription.created" ,
"timestamp" : "2024-01-01T10:00:00.000Z" ,
"idempotency_key" : "subscription.created:subscription-uuid-456" ,
"data" : {
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "ACTIVE" ,
"created_at" : "2024-01-01T10:00:00Z"
}
}
}
subscription.cancelled
When itβs sent: When a subscription is cancelled.
Example:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "subscription.cancelled" ,
"timestamp" : "2024-01-15T14:30:00.000Z" ,
"idempotency_key" : "subscription.cancelled:subscription-uuid-456" ,
"data" : {
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "CANCELLED" ,
"cancellation_date" : "2024-01-15T14:30:00Z"
}
}
}
subscription.expired
When itβs sent: When a subscription expires.
Example:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"event" : "subscription.expired" ,
"timestamp" : "2024-02-01T00:00:00.000Z" ,
"idempotency_key" : "subscription.expired:subscription-uuid-456" ,
"data" : {
"subscription" : {
"subscription_id" : "subscription-uuid-456" ,
"status" : "EXPIRED" ,
"expiration_date" : "2024-02-01T00:00:00Z"
}
}
}
Idempotency
Webhooks include an idempotency_key in both the payload and the X-Idempotency-Key header. This key is deterministic and follows the format: {event_type}:{entity_uuid}
Idempotency Key Format Example:
Event: transaction.authorized
Transaction UUID: transaction-uuid-123
Idempotency Key: transaction.authorized:transaction-uuid-123
Use the idempotency key to detect and handle duplicate webhook deliveries. If a webhook with the same idempotency key has already been processed, ignore it and return 200 OK.
Recommended Implementation
Store processed keys
Maintain a database or cache of processed idempotency keys
Check before processing
When a webhook arrives, check if the idempotency key exists
Handle duplicates
If the key exists, return 200 OK without processing
Process and store
If the key doesnβt exist, process the webhook and store the key
Example Implementation
def handle_webhook ( request ):
idempotency_key = request.headers[ 'X-Idempotency-Key' ]
# Check if already processed
if idempotency_key_already_processed(idempotency_key):
return Response( status = 200 ) # Already processed
# Process webhook
process_webhook(request.payload)
# Mark as processed
mark_idempotency_key_as_processed(idempotency_key)
return Response( status = 200 )
Retry Logic
TumiPay Card Payments implements automatic retry logic for failed webhook deliveries.
HTTP Status Codes
Webhooks are considered successful if your endpoint returns a 2xx HTTP status code (200-299). Any other status code will trigger a retry.
Return 200 OK immediately upon receiving the webhook. Process the webhook asynchronously if needed, but donβt wait for processing to complete before responding.
If all retry attempts fail, the webhook is marked as failed and will not be retried automatically. You can query webhook status through the TumiPay API if needed.
Best Practices
1. Verify Signatures Always verify the webhook signature before processing. This ensures authenticity and prevents tampering.
2. Handle Idempotency Use the idempotency key to prevent duplicate processing. Store processed keys and check them before processing.
3. Respond Quickly Return 200 OK as quickly as possible (within 20 seconds). Process webhooks asynchronously if necessary.
4. Log Everything Log all received webhooks for debugging and audit purposes. Include webhook ID, event type, and timestamp.
5. Validate Payloads Validate the payload structure before processing. Ensure required fields are present and have expected types.
6. Handle Errors Gracefully If processing fails, log the error but still return 200 OK to prevent unnecessary retries. Implement your own retry mechanism if needed.
7. Use HTTPS Always use HTTPS endpoints for webhook delivery. TumiPay only sends webhooks to secure URLs.
8. Monitor Delivery Monitor your webhook endpoint for availability and response times. Set up alerts for failures or delays.
Example Webhook Handlers
PHP Example
<? php
class WebhookHandler
{
private string $secretKey ;
public function __construct ( string $secretKey )
{
$this -> secretKey = $secretKey ;
}
public function handle ( Request $request ) : Response
{
// Extract headers
$signature = $request -> header ( 'X-Webhook-Signature' );
$idempotencyKey = $request -> header ( 'X-Idempotency-Key' );
$webhookId = $request -> header ( 'X-Webhook-Id' );
// Get raw payload
$payload = $request -> getContent ();
// Verify signature
if ( ! $this -> verifySignature ( $payload , $signature )) {
return response () -> json ([ 'error' => 'Invalid signature' ], 401 );
}
// Check idempotency
if ( $this -> isDuplicate ( $idempotencyKey )) {
return response () -> json ([ 'status' => 'already_processed' ], 200 );
}
// Parse payload
$data = json_decode ( $payload , true );
// Process webhook asynchronously
dispatch ( new ProcessWebhookJob ( $data ));
// Mark as processed
$this -> markAsProcessed ( $idempotencyKey );
// Log webhook
Log :: info ( 'Webhook received' , [
'webhook_id' => $webhookId ,
'event' => $data [ 'event' ],
'idempotency_key' => $idempotencyKey ,
]);
return response () -> json ([ 'status' => 'received' ], 200 );
}
private function verifySignature ( string $payload , string $signature ) : bool
{
$expectedSignature = hash_hmac ( 'sha256' , $payload , $this -> secretKey );
return hash_equals ( $expectedSignature , $signature );
}
private function isDuplicate ( string $idempotencyKey ) : bool
{
return Cache :: has ( "webhook:{ $idempotencyKey }" );
}
private function markAsProcessed ( string $idempotencyKey ) : void
{
Cache :: put ( "webhook:{ $idempotencyKey }" , true , now () -> addDays ( 7 ));
}
}
Node.js Example
const crypto = require ( 'crypto' );
const express = require ( 'express' );
const app = express ();
const secretKey = process . env . WEBHOOK_SECRET_KEY ;
const processedKeys = new Set ();
app . use ( express . raw ({ type: 'application/json' }));
app . post ( '/webhooks' , ( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ];
const idempotencyKey = req . headers [ 'x-idempotency-key' ];
const webhookId = req . headers [ 'x-webhook-id' ];
const payload = req . body . toString ();
// Verify signature
if ( ! verifySignature ( payload , signature , secretKey )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Check idempotency
if ( processedKeys . has ( idempotencyKey )) {
return res . status ( 200 ). json ({ status: 'already_processed' });
}
// Parse payload
const data = JSON . parse ( payload );
// Process webhook asynchronously
processWebhook ( data );
// Mark as processed
processedKeys . add ( idempotencyKey );
// Log webhook
console . log ( 'Webhook received' , {
webhook_id: webhookId ,
event: data . event ,
idempotency_key: idempotencyKey ,
});
res . status ( 200 ). json ({ status: 'received' });
});
function verifySignature ( payload , signature , secretKey ) {
const expectedSignature = crypto
. createHmac ( 'sha256' , secretKey )
. update ( payload )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
);
}
function processWebhook ( data ) {
// Process webhook asynchronously
// e.g., update database, send notifications, etc.
}
app . listen ( 3000 );
Support
For questions or issues related to webhooks, please contact:
Or refer to the main Card Payment API documentation for additional information.