Skip to main content

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:
  1. Contact Support: Send an email to [email protected] or request assistance through the support channel
  2. Provide Your Webhook URL: Your HTTPS endpoint where webhooks will be sent (must be publicly accessible)
  3. Receive Secret Key: You will receive a shared secret key used to sign and verify webhook payloads
  4. 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.

HTTP Headers

Every webhook request includes specific HTTP headers that provide metadata and security information.

Webhook Headers

Example Request Headers

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.

Signature Format

The signature is a hexadecimal string (64 characters) representing the HMAC SHA256 hash. Example: c829a84733035c391f71cfeb73113a6b3161eac74f37e97c930bc44040c8e515

Verification Process

1

Extract the signature

Get the signature from the X-Webhook-Signature header (it’s a hexadecimal string)
2

Compute expected signature

Create HMAC SHA256 of the raw JSON payload using your secret key
3

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:
FieldTypeDescription
idstringUnique identifier for this webhook (UUID)
eventstringEvent type identifier (e.g., transaction.authorized, subscription.created)
timestampstringISO 8601 timestamp in UTC format
idempotency_keystringKey for duplicate detection (format: {event}:{entity_uuid})
dataobjectEvent-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.
1

Store processed keys

Maintain a database or cache of processed idempotency keys
2

Check before processing

When a webhook arrives, check if the idempotency key exists
3

Handle duplicates

If the key exists, return 200 OK without processing
4

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.

Retry Configuration

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.