# Webhooks 🔔

Webhooks provide real-time notifications when events occur in your CardScan.ai account, enabling you to build responsive applications that react immediately to card scanning and eligibility verification events.

## Introduction

Webhooks are how services notify each other of events. At their core, they are just a POST request to a pre-determined endpoint that you control.

The endpoint can be whatever you want, and you can configure them from the [CardScan Dashboard](https://dashboard.cardscan.ai). You normally use one endpoint per service, and that endpoint listens to all of the event types you're interested in.

For example, if you receive webhooks from CardScan.ai, you can structure your URL like: `https://www.example.com/cardscan/webhooks/`.

The way to indicate that a webhook has been processed successfully is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (15 seconds).

{% hint style="warning" %}
It's important to disable CSRF protection for your webhook endpoint if your framework enables it by default.
{% endhint %}

Another critical aspect of handling webhooks is to verify the signature and timestamp when processing them. You can learn more about this in the [signature verification](#signature-verification) section.

## Webhook Payload Structure

{% hint style="info" %}
**Security & Privacy:** Webhook payloads contain only event metadata and identifiers. Detailed card information and eligibility results are **not** included in webhook payloads for security reasons. You'll need to use the [API](https://docs.cardscan.ai/api) to fetch full details using the provided IDs.
{% endhint %}

All webhook events follow a consistent structure:

```json
{
  "type": "card.completed",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:41:06.845194+00:00",
  "user_id": "postman_hotpath"
}
```

## Available Events

CardScan.ai sends webhooks for card scanning and eligibility verification events. Each webhook includes event metadata, but you'll need to call our API to get the full details.

### Card Events

#### card.created

Triggered when a new insurance card is created at the start of a scanning attempt.

```json
{
  "type": "card.created",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:40:39.522710+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.completed

Triggered after a successful insurance card scan. Use the `card_id` to fetch the extracted data via the [Get Card API](https://docs.cardscan.ai/api#get-card).

```json
{
  "type": "card.completed",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:41:06.845194+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.error

Triggered when an error occurs during an insurance card scan.

```json
{
  "type": "card.error",
  "card_id": "b6d83d59-9577-4cff-9df2-b6b667259817",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 03:45:22.479359+00:00",
  "deleted": false,
  "error": {
    "code": "UnknownError",
    "message": "Unknown processing error, please try again. If the problem persists, please contact support.",
    "type": "UnknownError"
  },
  "session_id": "ses_5952a878-c58d-4eb0-af55-2ca35aee6bd3",
  "updated_at": "2025-07-02 03:45:37.080034+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.deleted

Triggered when a scanned insurance card is marked as deleted.

```json
{
  "type": "card.deleted",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": true,
  "deleted_at": "2025-07-02 16:30:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 16:30:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

### Eligibility Events

{% hint style="info" %}
Eligibility webhooks require the [Eligibility Verification](https://docs.cardscan.ai/advanced-features/eligibility-verification) feature to be enabled on your account.
{% endhint %}

#### eligibility.created

Triggered when a new eligibility record is created for an insurance card.

```json
{
  "type": "eligibility.created",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "pending",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:45:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.completed

Triggered when an eligibility check for an insurance card is successfully completed. Use the `eligibility_id` to fetch the full eligibility details via the API.

```json
{
  "type": "eligibility.completed",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "completed",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:47:21.123456+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.error

Triggered when an error occurs during an eligibility check.

```json
{
  "type": "eligibility.error",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "error",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "error": {
    "code": "PayerNotSupported",
    "message": "Unable to verify eligibility with payer",
    "type": "PayerNotSupported"
  },
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:47:30.789012+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.deleted

Triggered when an eligibility record is deleted.

```json
{
  "type": "eligibility.deleted",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "deleted": true,
  "deleted_at": "2025-07-02 16:30:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 16:30:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

## Processing Webhook Events

Since webhook payloads only contain event metadata, you'll typically follow this pattern:

### Example: Processing a Card Completion

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { CardScanApi } from '@cardscan.ai/cardscan-client';

app.post('/webhooks/cardscan', async (req, res) => {
  try {
    // Verify the webhook signature first
    const payload = wh.verify(req.body, req.headers);
    
    // Acknowledge the webhook immediately
    res.status(200).send('OK');
    
    // Process the event asynchronously
    if (payload.type === 'card.completed') {
      const { card_id, user_id } = payload;
      
      // Fetch the full card details using the API
      const client = new CardScanApi({
        sessionToken: await getSessionTokenForUser(user_id),
        live: true
      });
      
      const cardDetails = await client.getCard(card_id);
      
      // Process the card data
      await processCompletedCard(cardDetails);
    }
  } catch (err) {
    console.error('Webhook processing failed:', err);
    res.status(400).send('Invalid signature');
  }
});
```

{% endtab %}

{% tab title="Python" %}

```python
import requests
from cardscan_client import CardScanApi

@app.route('/webhooks/cardscan', methods=['POST'])
def handle_webhook():
    try:
        # Verify the webhook signature first
        payload = wh.verify(request.data, dict(request.headers))
        
        # Acknowledge the webhook immediately
        response = make_response('OK', 200)
        
        # Process the event asynchronously
        if payload['type'] == 'card.completed':
            card_id = payload['card_id']
            user_id = payload['user_id']
            
            # Fetch the full card details using the API
            session_token = get_session_token_for_user(user_id)
            client = CardScanApi(session_token=session_token, live=True)
            
            card_details = client.get_card(card_id)
            
            # Process the card data
            process_completed_card(card_details)
            
        return response
    except Exception as e:
        print(f"Webhook processing failed: {e}")
        return make_response('Invalid signature', 400)
```

{% endtab %}
{% endtabs %}

## API Client Libraries

To make processing webhook events easier, use our official API client libraries that include typed models for parsing responses:

{% embed url="<https://github.com/CardScan-ai/api-clients/tree/main>" %}

Available clients:

* **TypeScript/JavaScript** - `@cardscan.ai/cardscan-client`
* **Python** - `cardscan-client`
* **Swift** - Swift Package Manager
* **Kotlin** - `com.cardscan:api`
* **Dart** - `cardscan_client`

These clients provide strongly-typed models for card details, eligibility results, and API responses, making it easier to work with the data you fetch after receiving webhook notifications.

## Adding Webhook Endpoints

To start receiving webhook notifications, you need to configure your endpoints in the CardScan Dashboard.

### Dashboard Configuration

1. Log into your [CardScan Dashboard](https://dashboard.cardscan.ai)
2. Navigate to the **Webhooks** section
3. Click **Add Endpoint**
4. Enter your webhook URL (e.g., `https://your-domain.com/webhooks/cardscan`)
5. Select the event types you want to receive
6. Click **Create Endpoint**

{% hint style="info" %}
If you don't specify any event types, your endpoint will receive all events by default. We recommend selecting specific event types to avoid receiving unnecessary messages.
{% endhint %}

{% hint style="warning" %}
**API Configuration Coming Soon:** While webhook endpoints are currently managed through the dashboard, API-based endpoint management is available upon request. Contact <team@cardscan.ai> if you need programmatic webhook management.
{% endhint %}

## Testing Webhooks

Once you've added an endpoint, you should test it to ensure it's working correctly.

### Dashboard Testing

1. Go to your webhook endpoint in the dashboard
2. Click on the **Testing** tab
3. Select an event type to test
4. Click **Send Test Event**
5. Review the response and check your endpoint logs

After sending a test event, you can click into the message to view:

* The complete message payload
* All delivery attempts
* Success/failure status
* Response details

### Test Event Payloads

Test events use the same structure as real events but with clearly marked test data:

```json
{
  "type": "card.completed",
  "card_id": "test_card_12345",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 15:50:14.856792+00:00",
  "deleted": false,
  "session_id": "test_session",
  "updated_at": "2025-07-02 15:50:14.856792+00:00",
  "user_id": "test_user"
}
```

## Signature Verification

Webhook signatures let you verify that webhook messages are actually sent by CardScan.ai and not a malicious actor. **You should always verify webhook signatures in production.**

### Why Verify Signatures?

Without signature verification, any malicious actor could send fake webhook events to your endpoint, potentially compromising your application's security and data integrity.

### How to Verify

CardScan.ai uses [Svix](https://svix.com) for webhook delivery, which provides battle-tested signature verification. You can use Svix's libraries to easily verify webhook signatures:

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { Webhook } from "svix";

const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

// These headers are sent with every webhook
const headers = {
  "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
  "svix-timestamp": "1614265330",
  "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"type": "card.completed", "data": {...}}';

const wh = new Webhook(secret);
try {
  // Throws on error, returns the verified content on success
  const verifiedPayload = wh.verify(payload, headers);
  console.log("Webhook verified successfully:", verifiedPayload);
} catch (err) {
  console.error("Webhook verification failed:", err.message);
}
```

{% endtab %}

{% tab title="Python" %}

```python
from svix.webhooks import Webhook

secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

headers = {
    "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
    "svix-timestamp": "1614265330",
    "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
}
payload = '{"type": "card.completed", "data": {...}}'

wh = Webhook(secret)
try:
    # Throws on error, returns the verified content on success
    verified_payload = wh.verify(payload, headers)
    print("Webhook verified successfully:", verified_payload)
except Exception as e:
    print("Webhook verification failed:", str(e))
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
    "fmt"
    "github.com/svix/svix-webhooks/go"
)

func main() {
    secret := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
    
    headers := map[string]string{
        "svix-id":        "msg_p5jXN8AQM9LWM0D4loKWxJek",
        "svix-timestamp": "1614265330",
        "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
    }
    payload := `{"type": "card.completed", "data": {...}}`
    
    wh, err := svix.NewWebhook(secret)
    if err != nil {
        panic(err)
    }
    
    verifiedPayload, err := wh.Verify(payload, headers)
    if err != nil {
        fmt.Printf("Webhook verification failed: %v\n", err)
        return
    }
    
    fmt.Printf("Webhook verified successfully: %s\n", verifiedPayload)
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For more examples and supported languages, check out the [Svix webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how).
{% endhint %}

### Finding Your Webhook Secret

Each webhook endpoint has a unique secret key that you can find in the CardScan Dashboard:

1. Go to your webhook endpoint settings
2. Click **Signing Secret**
3. Copy the secret (it starts with `whsec_`)

## Retry Schedule

CardScan.ai automatically retries failed webhook deliveries using an exponential backoff strategy to ensure reliable delivery.

### Retry Attempts

Each webhook message is attempted based on the following schedule:

* **Immediately**
* **5 seconds**
* **5 minutes**
* **30 minutes**
* **2 hours**
* **5 hours**
* **10 hours**
* **10 hours** (final attempt)

For example, a webhook that fails three times before succeeding will be delivered approximately 35 minutes and 5 seconds after the first attempt.

### Automatic Disabling

If all delivery attempts to an endpoint fail for a period of **5 days**, the endpoint will be automatically disabled to prevent unnecessary retry attempts.

### Manual Retries

You can manually retry webhook deliveries from the dashboard:

**Single Message Retry:**

1. Find the failed message in your webhook endpoint logs
2. Click the options menu next to the failed attempt
3. Select **Resend** to retry the delivery

**Bulk Recovery:**

1. Go to your endpoint's details page
2. Click **Options > Recover Failed Messages**
3. Choose a time window to recover from
4. All failed messages in that period will be resent

## Troubleshooting

Here are common issues and solutions when working with CardScan.ai webhooks:

### Common Problems

#### Not Using Raw Payload Body

**Most common issue.** When verifying signatures, you must use the raw string body of the webhook payload exactly as received. Don't parse it as JSON first.

```javascript
// ❌ Wrong - don't parse JSON first
const parsedBody = JSON.parse(requestBody);
const stringifiedBody = JSON.stringify(parsedBody);
wh.verify(stringifiedBody, headers); // This will fail

// ✅ Correct - use raw body
wh.verify(requestBody, headers);
```

#### Wrong Secret Key

Make sure you're using the correct secret for your specific endpoint. Each endpoint has its own unique secret key.

#### Incorrect Response Codes

Return 2xx status codes (200-299) for successful webhook processing, even if your business logic determines the event should be ignored.

```javascript
// ✅ Correct
app.post('/webhooks/cardscan', (req, res) => {
  try {
    const payload = wh.verify(req.body, req.headers);
    // Process the webhook...
    res.status(200).send('OK');
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});
```

#### Response Timeouts

Webhooks must respond within **15 seconds**. For complex processing, acknowledge the webhook immediately and process asynchronously:

```javascript
app.post('/webhooks/cardscan', async (req, res) => {
  try {
    const payload = wh.verify(req.body, req.headers);
    
    // Acknowledge immediately
    res.status(200).send('OK');
    
    // Process asynchronously
    processWebhookAsync(payload);
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});
```

### Failure Recovery

#### Re-enable a Disabled Endpoint

If your endpoint was disabled due to consecutive failures:

1. Fix the underlying issue with your endpoint
2. Go to the webhook dashboard
3. Find your endpoint and click **Enable Endpoint**

#### Replay Failed Messages

To recover from downtime or misconfigurations:

1. **Single Event:** Find the message and click **Resend**
2. **Time Range:** Use **Options > Recover Failed Messages** to replay all failed events from a specific time period
3. **From Specific Message:** Click the options menu on any message and select **Replay all failed messages since this time**

{% hint style="info" %}
Need help with your webhook implementation? Contact us at <team@cardscan.ai> and we'll help you get set up correctly.
{% endhint %}
