> ## Documentation Index
> Fetch the complete documentation index at: https://docs.v2.topup.com.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications about Card Payment transactions and subscription events.

# 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.

<Card title="What are Webhooks?" icon="question-circle">
  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.
</Card>

## 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.

<Card title="How to Configure Webhooks" icon="envelope">
  To set up webhook notifications, you need to:

  1. **Contact Support**: Send an email to [soporte@tumipay.co](mailto:soporte@tumipay.co) 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
</Card>

<CardGroup cols={3}>
  <Card title="Webhook URL" icon="link">
    Your HTTPS endpoint where webhooks will be sent. Must be publicly accessible.
  </Card>

  <Card title="Secret Key" icon="key">
    A shared secret used to sign and verify webhook payloads. Keep this secure!
  </Card>

  <Card title="Status" icon="toggle-on">
    Active or inactive. Only one active webhook configuration per merchant is supported.
  </Card>
</CardGroup>

<Info>
  For webhook configuration assistance, contact:

  * **Support Email**: [soporte@tumipay.co](mailto:soporte@tumipay.co)
  * **Technical Email**: [tecnologia@tumipay.co](mailto:tecnologia@tumipay.co)
</Info>

<Warning>
  Your webhook URL must use HTTPS. TumiPay will only send webhooks to secure endpoints.
</Warning>

***

## HTTP Headers

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

<Card title="Webhook Headers" icon="list">
  <Properties>
    <Property name="Content-Type" type="string" required>
      Always `application/json`
    </Property>

    <Property name="X-Webhook-Event" type="string" required>
      The event type identifier (e.g., `transaction.authorized`)
    </Property>

    <Property name="X-Webhook-Timestamp" type="string" required>
      ISO 8601 timestamp in UTC format (e.g., `2024-01-01T10:00:00.000Z`)
    </Property>

    <Property name="X-Webhook-Id" type="string" required>
      Unique identifier for this webhook (UUID)
    </Property>

    <Property name="X-Idempotency-Key" type="string" required>
      Key for duplicate detection (format: `{event}:{entity_uuid}`)
    </Property>

    <Property name="X-Webhook-Signature" type="string" required>
      HMAC SHA256 signature for verification (hexadecimal string, 64 characters)
    </Property>

    <Property name="User-Agent" type="string" required>
      Client identifier: `TumiPay-Webhooks/1.0`
    </Property>
  </Properties>
</Card>

### Example Request Headers

```http theme={null}
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.

<Warning>
  Always verify the webhook signature before processing. This ensures the webhook originated from TumiPay and has not been tampered with.
</Warning>

### Signature Format

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

Example: `c829a84733035c391f71cfeb73113a6b3161eac74f37e97c930bc44040c8e515`

### Verification Process

<Steps>
  <Step title="Extract the signature">
    Get the signature from the `X-Webhook-Signature` header (it's a hexadecimal string)
  </Step>

  <Step title="Compute expected signature">
    Create HMAC SHA256 of the raw JSON payload using your secret key
  </Step>

  <Step title="Compare securely">
    Use constant-time comparison to compare signatures (prevents timing attacks)
  </Step>
</Steps>

### Code Examples

<CodeGroup>
  ```php PHP theme={null}
  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);
  }
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(payload, signature, secretKey) {
      const expectedSignature = crypto
          .createHmac('sha256', secretKey)
          .update(payload)
          .digest('hex');
      
      // Constant-time comparison
      return crypto.timingSafeEqual(
          Buffer.from(signature, 'hex'),
          Buffer.from(expectedSignature, 'hex')
      );
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook_signature(payload, signature, secret_key):
      expected_signature = hmac.new(
          secret_key.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()
      
      # Constant-time comparison
      return hmac.compare_digest(expected_signature, signature)
  ```
</CodeGroup>

<Tip>
  Always use constant-time comparison functions (`hash_equals`, `timingSafeEqual`, `compare_digest`) to prevent timing attacks. Never use simple string comparison (`==` or `===`).
</Tip>

***

## 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:

```json theme={null}
{
  "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                                                                    |
| ----------------- | ------ | ------------------------------------------------------------------------------ |
| `id`              | string | Unique identifier for this webhook (UUID)                                      |
| `event`           | string | Event type identifier (e.g., `transaction.authorized`, `subscription.created`) |
| `timestamp`       | string | ISO 8601 timestamp in UTC format                                               |
| `idempotency_key` | string | Key for duplicate detection (format: `{event}:{entity_uuid}`)                  |
| `data`            | object | 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:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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"
    }
  }
}
```

<Info>
  **Note:** The `subscription` object is optional and may not be present if the transaction is not associated with a subscription.
</Info>

***

### transaction.captured

**When it's sent:** When a transaction is successfully captured (payment completed).

**Example:**

```json theme={null}
{
  "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"
    }
  }
}
```

<Info>
  **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.
</Info>

***

### transaction.declined

**When it's sent:** When a transaction is declined by the payment provider.

**Example:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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}`

<Card title="Idempotency Key Format" icon="key">
  **Example:**

  * Event: `transaction.authorized`
  * Transaction UUID: `transaction-uuid-123`
  * Idempotency Key: `transaction.authorized:transaction-uuid-123`
</Card>

<Tip>
  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.
</Tip>

### Recommended Implementation

<Steps>
  <Step title="Store processed keys">
    Maintain a database or cache of processed idempotency keys
  </Step>

  <Step title="Check before processing">
    When a webhook arrives, check if the idempotency key exists
  </Step>

  <Step title="Handle duplicates">
    If the key exists, return 200 OK without processing
  </Step>

  <Step title="Process and store">
    If the key doesn't exist, process the webhook and store the key
  </Step>
</Steps>

### Example Implementation

```python theme={null}
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.

<Card title="Retry Configuration" icon="clock">
  <Properties>
    <Property name="Maximum Attempts" type="number">
      3 attempts total
    </Property>

    <Property name="First Retry" type="string">
      60 seconds (1 minute) after initial attempt
    </Property>

    <Property name="Second Retry" type="string">
      600 seconds (10 minutes) after first retry
    </Property>

    <Property name="Third Retry" type="string">
      1800 seconds (30 minutes) after second retry
    </Property>

    <Property name="Timeout" type="string">
      20 seconds per request
    </Property>
  </Properties>
</Card>

### 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.

<Tip>
  Return `200 OK` immediately upon receiving the webhook. Process the webhook asynchronously if needed, but don't wait for processing to complete before responding.
</Tip>

<Warning>
  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.
</Warning>

***

## Best Practices

<CardGroup cols={2}>
  <Card title="1. Verify Signatures" icon="shield-halved">
    Always verify the webhook signature before processing. This ensures authenticity and prevents tampering.
  </Card>

  <Card title="2. Handle Idempotency" icon="repeat">
    Use the idempotency key to prevent duplicate processing. Store processed keys and check them before processing.
  </Card>

  <Card title="3. Respond Quickly" icon="bolt">
    Return 200 OK as quickly as possible (within 20 seconds). Process webhooks asynchronously if necessary.
  </Card>

  <Card title="4. Log Everything" icon="file-lines">
    Log all received webhooks for debugging and audit purposes. Include webhook ID, event type, and timestamp.
  </Card>

  <Card title="5. Validate Payloads" icon="check-double">
    Validate the payload structure before processing. Ensure required fields are present and have expected types.
  </Card>

  <Card title="6. Handle Errors Gracefully" icon="triangle-exclamation">
    If processing fails, log the error but still return 200 OK to prevent unnecessary retries. Implement your own retry mechanism if needed.
  </Card>

  <Card title="7. Use HTTPS" icon="lock">
    Always use HTTPS endpoints for webhook delivery. TumiPay only sends webhooks to secure URLs.
  </Card>

  <Card title="8. Monitor Delivery" icon="chart-line">
    Monitor your webhook endpoint for availability and response times. Set up alerts for failures or delays.
  </Card>
</CardGroup>

***

## Example Webhook Handlers

### PHP Example

```php theme={null}
<?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

```javascript theme={null}
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:

* **Technical Support**: [tecnologia@tumipay.co](mailto:tecnologia@tumipay.co)
* **General Support**: [soporte@tumipay.co](mailto:soporte@tumipay.co)

Or refer to the main [Card Payment API documentation](/api-reference/card-payment) for additional information.
