# Webhooks

Webhooks allow your application to receive real-time notifications about events that occur on the Monetarie NPC platform. Instead of polling the API periodically, you automatically receive data via HTTP POST when an event happens.

## How It Works

```
Monetarie NPC                           Your Application
       |                                   |
       |  1. Event occurs (e.g., boleto paid)
       |                                   |
       |  2. HTTP POST with JSON payload   |
       |---------------------------------->|
       |                                   |
       |  3. Process event                 |
       |                                   |
       |  4. Respond 200 OK                |
       |<----------------------------------|
       |                                   |
```

## Configuration

Configure your webhook URL in the [Monetarie Portal](https://npcadmin-dev.fluxiq.com.br) or via the configuration API:

```bash
curl -X PUT "https://api.monetarie_npc.com.br/api/v1/central/config/central" \
  -H "X-API-Key: pk_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-application.com/webhooks/monetarie_npc",
    "enabled": true
  }'
```

## URL Requirements

Your webhook URL must meet the following requirements:

| Requirement | Description |
|-------------|-------------|
| **Protocol** | HTTPS required (HTTP not accepted) |
| **Timeout** | Respond within 30 seconds |
| **Status Code** | Return 2xx code for success |
| **Availability** | URL must be publicly accessible |
| **Certificate** | Valid SSL/TLS (self-signed certificates not accepted) |

::: warning HTTPS Required
For security reasons, only HTTPS URLs are accepted. Requests to HTTP will be rejected.
:::

## Security

### HMAC-SHA256 Signature

All webhook requests include an HMAC-SHA256 signature to ensure authenticity and integrity. You must validate this signature before processing any event.

### Security Headers

| Header | Description |
|--------|-------------|
| `X-Webhook-Signature` | HMAC-SHA256 signature of the payload |
| `X-Webhook-Timestamp` | Unix timestamp of the request |
| `X-Request-Id` | Unique request identifier |

### Signature Format

The signature is calculated using:

```
signature = HMAC-SHA256(webhook_secret, timestamp + "." + payload)
```

Where:
- `webhook_secret` is your configured secret key
- `timestamp` is the value from the `X-Webhook-Timestamp` header
- `payload` is the JSON request body (raw body)

### Signature Validation

::: code-group

```javascript [Node.js]
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp, secret) {
  // 1. Verify timestamp (5-minute window)
  const currentTime = Math.floor(Date.now() / 1000);
  const webhookTime = parseInt(timestamp, 10);

  if (Math.abs(currentTime - webhookTime) > 300) {
    throw new Error('Timestamp outside tolerance window');
  }

  // 2. Calculate expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // 3. Compare signatures (timing-safe)
  const signatureBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expectedSignature, 'hex');

  if (!crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
    throw new Error('Invalid signature');
  }

  return true;
}

// Usage with Express.js
app.post('/webhooks/monetarie_npc', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const payload = req.body.toString();

  try {
    verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET);
    const event = JSON.parse(payload);
    // Process event...
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook verification failed:', error.message);
    res.status(401).send('Unauthorized');
  }
});
```

```python [Python]
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)

def verify_webhook_signature(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    # 1. Verify timestamp (5-minute window)
    current_time = int(time.time())
    webhook_time = int(timestamp)

    if abs(current_time - webhook_time) > 300:
        raise ValueError('Timestamp outside tolerance window')

    # 2. Calculate expected signature
    signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # 3. Compare signatures (timing-safe)
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Invalid signature')

    return True

@app.route('/webhooks/monetarie_npc', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    payload = request.get_data()

    try:
        verify_webhook_signature(
            payload,
            signature,
            timestamp,
            os.environ['WEBHOOK_SECRET']
        )
        event = request.get_json()
        # Process event...
        return 'OK', 200
    except ValueError as e:
        print(f'Webhook verification failed: {e}')
        abort(401)
```

```php [PHP]
<?php

function verifyWebhookSignature(
    string $payload,
    string $signature,
    string $timestamp,
    string $secret
): bool {
    // 1. Verify timestamp (5-minute window)
    $currentTime = time();
    $webhookTime = (int) $timestamp;

    if (abs($currentTime - $webhookTime) > 300) {
        throw new Exception('Timestamp outside tolerance window');
    }

    // 2. Calculate expected signature
    $signedPayload = $timestamp . '.' . $payload;
    $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

    // 3. Compare signatures (timing-safe)
    if (!hash_equals($expectedSignature, $signature)) {
        throw new Exception('Invalid signature');
    }

    return true;
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = getenv('WEBHOOK_SECRET');

try {
    verifyWebhookSignature($payload, $signature, $timestamp, $secret);
    $event = json_decode($payload, true);
    // Process event...
    http_response_code(200);
    echo 'OK';
} catch (Exception $e) {
    error_log('Webhook verification failed: ' . $e->getMessage());
    http_response_code(401);
    echo 'Unauthorized';
}
```

:::

### Timestamp Validation

Timestamp validation prevents replay attacks. Reject requests where the timestamp differs by more than 5 minutes from the current time:

```
|current_time - webhook_timestamp| <= 300 seconds (5 minutes)
```

::: tip Clock Synchronization
Make sure your server clock is synchronized via NTP to avoid rejecting legitimate webhooks.
:::

## Retry Policy

If webhook delivery fails (timeout, connection error, or status code other than 2xx), we retry with exponential backoff:

| Attempt | Interval | Total Time |
|---------|----------|------------|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 15 minutes | 21 min |
| 5 | 1 hour | 1h 21min |
| 6 | 4 hours | 5h 21min |

After 6 unsuccessful attempts, the webhook is marked as failed and will not be retried again. You can view failed webhooks in the portal and trigger them manually if necessary.

::: warning Guaranteed Delivery
Webhooks are sent with "at-least-once delivery" guarantee. This means in rare cases, you may receive the same event more than once. Use the `X-Request-Id` to implement idempotency.
:::

## Idempotency

To ensure your application processes each event only once, use the `X-Request-Id` header:

1. **Store the ID** of each successfully processed webhook
2. **Check for duplicates** before processing new webhooks
3. **Return 200** for duplicates (indicating it was already processed)

```javascript
// Idempotency check example
const processedWebhooks = new Set(); // Use Redis in production

app.post('/webhooks/monetarie_npc', async (req, res) => {
  const requestId = req.headers['x-request-id'];

  // Check if we already processed this webhook
  if (processedWebhooks.has(requestId)) {
    console.log(`Webhook ${requestId} already processed, ignoring`);
    return res.status(200).send('OK');
  }

  // Process webhook...

  // Mark as processed
  processedWebhooks.add(requestId);

  res.status(200).send('OK');
});
```

::: tip Redis for Idempotency
In production, use Redis or a database to store processed webhook IDs. Configure an expiration (TTL) of at least 24 hours.
:::

## Testing Webhooks

### Sandbox Environment

In the sandbox environment, you can use tools like [webhook.site](https://webhook.site) to inspect payloads:

1. Go to [webhook.site](https://webhook.site) and copy your unique URL
2. Configure this URL in the Monetarie Portal (sandbox)
3. Execute actions in sandbox (create boleto, etc.)
4. View received webhooks in real-time

### Test Endpoint

Use the test endpoint to trigger example webhooks:

```bash
curl -X POST "https://sandbox.monetarie_npc.com.br/api/v1/central/webhooks/test" \
  -H "X-API-Key: pk_test_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "boleto_paid"
  }'
```

## Next Steps

- [Events](/en/webhooks/events) - Event types and payloads
- [Implementation](/en/webhooks/handling) - Complete implementation guide
- [Boleto Flow](/en/guides/boleto-flow) - Understand the boleto lifecycle
