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

# Webhooks Guide

> Learn how to receive real-time notifications from Revolv3 via webhooks. Understand webhook events, how to verify signatures, and handle different event types.

## What are Webhooks?

**Webhooks** are HTTP callbacks that Revolv3 sends to your server when events happen. Instead of you constantly checking ("polling") the API to see if something changed, Revolv3 automatically notifies you when events occur.

**Why use webhooks:**

* **Real-time updates**: Know immediately when payments succeed or fail
* **Efficient**: No need to constantly poll the API
* **Better UX**: Update your UI instantly when events happen
* **Essential for ACH**: ACH payments take days—webhooks tell you when they complete

## How Webhooks Work

### The Flow

1. **Event happens** in Revolv3 (payment succeeds, invoice status changes, etc.)
2. **Revolv3 sends HTTP POST** to your webhook URL
3. **Your server receives** the webhook and processes it
4. **Your server responds** with 200 OK to confirm receipt
5. **You update your system** based on the event data

### Webhook Format

Revolv3 sends webhooks with this structure:

```json theme={null}
{
  "Body": "{\"Invoice\":{\"InvoiceId\":330973,\"InvoiceStatus\":\"Paid\"...},\"EventDateTime\":\"2025-01-27T18:18:48Z\",\"EventType\":\"InvoiceStatusChanged\",\"RevolvMerchantId\":579}",
  "Entropy": "b39d4914-cb32-46d0-b3ab-e832539d3d7b"
}
```

**Understanding the fields:**

* `Body`: A JSON string containing the event data (you need to parse it)
* `Entropy`: A random string for additional security

**`The Body field is a string`**, so you need to parse it as JSON to access the data inside.

## Available Webhook Events

Revolv3 sends webhooks for these events:

| Event Type                    | When It's Sent                       | What It Means                                |
| ----------------------------- | ------------------------------------ | -------------------------------------------- |
| `SubscriptionCreated`         | Subscription is successfully created | New subscription is active                   |
| `SubscriptionChanged`         | Subscription is updated              | Subscription details changed                 |
| `SubscriptionFailed`          | Subscription creation failed         | Couldn't create the subscription             |
| `InvoiceCreated`              | New invoice is created               | Invoice ready for payment                    |
| `InvoiceStatusChanged`        | Invoice status changes               | Payment succeeded, failed, or status updated |
| `InvoiceAttemptCreated`       | Payment attempt is made              | Revolv3 tried to process a payment           |
| `InvoiceAttemptStatusChanged` | Payment attempt status changes       | Attempt succeeded or failed                  |
| `WebhookTest`                 | During webhook setup                 | Test event to verify your endpoint works     |

> **Note**: The `ACHInvoiceStatusChanged` webhook is deprecated. Use `InvoiceStatusChanged` instead—it covers all payment types including ACH.

## Understanding Event Data

### Parsing the Body

The `Body` field is a JSON string, so you need to parse it:

**Before parsing** (what you receive):

```json theme={null}
{
  "Body": "{\"Invoice\":{\"InvoiceId\":330973...},\"EventType\":\"InvoiceStatusChanged\"}",
  "Entropy": "b39d4914-cb32-46d0-b3ab-e832539d3d7b"
}
```

**After parsing** (what you use):

```json theme={null}
{
  "Invoice": {
    "InvoiceId": 330973,
    "InvoiceStatus": "Paid",
    "Subtotal": 0.13,
    "Total": 0.13
  },
  "EventDateTime": "2025-01-27T18:18:48.3105823Z",
  "EventType": "InvoiceStatusChanged",
  "RevolvMerchantId": 579
}
```

### Event Data Structure

Each event includes:

* `Event-specific data`: The main object (Invoice, Subscription, Attempt, etc.)
* `EventDateTime`: When the event occurred (ISO 8601 timestamp)
* `EventType`: The type of event (see table above)
* `RevolvMerchantId`: Your merchant ID (useful for multi-merchant setups)

## Webhook Examples

### Subscription Created

**When**: A subscription is successfully created

**Body** (after parsing):

```json theme={null}
{
  "Subscription": {
    "SubscriptionId": 2691,
    "CustomerId": 9687,
    "MerchantSubscriptionRefId": "1234-5678-9101",
    "SubscriptionStatusType": "Current",
    "NextBillDate": "1/28/2025",
    "BillingPlans": [...]
  },
  "EventDateTime": "2025-01-28T18:09:27.6118027Z",
  "EventType": "SubscriptionCreated",
  "RevolvMerchantId": 579
}
```

**What to do**: Store the subscription ID, update your system, notify the customer.

### Invoice Status Changed

**When**: An invoice status changes (e.g., from Pending to Paid)

**Body** (after parsing):

```json theme={null}
{
  "Invoice": {
    "InvoiceId": 330973,
    "InvoiceStatus": "Paid",
    "Subtotal": 0.13,
    "Total": 0.13,
    "NetworkTransactionId": "480213756277396"
  },
  "EventDateTime": "2025-01-27T18:18:48.3105823Z",
  "EventType": "InvoiceStatusChanged",
  "RevolvMerchantId": 579
}
```

**What to do**:

* If status is "Paid": Fulfill the order, send confirmation
* If status is "Noncollectable": Notify customer, ask for different payment method

### Invoice Attempt Status Changed

**When**: A payment attempt's status changes

**Body** (after parsing):

```json theme={null}
{
  "Attempt": {
    "InvoiceId": 331122,
    "InvoiceAttemptStatus": "Fail",
    "Amount": 10.99,
    "ResponseMessage": "Decline"
  },
  "EventDateTime": "2025-01-28T18:15:49.7836702Z",
  "EventType": "InvoiceAttemptStatusChanged",
  "RevolvMerchantId": 579
}
```

**What to do**: Handle the payment failure, notify customer, potentially retry.

## Webhook Security: Signature Verification

Revolv3 includes a signature in the `x-revolv3-signature` header so you can verify the webhook came from Revolv3.

### Why Verify Signatures?

**Security**: Verifying signatures ensures:

* The webhook actually came from Revolv3 (not a malicious third party)
* The data wasn't tampered with in transit
* You're processing legitimate events

**Always verify signatures** before processing webhook data.

### How Signature Verification Works

Revolv3 uses **HMAC-SHA256** to create a signature:

1. Concatenate your webhook URL + `$` + request body
2. Create HMAC-SHA256 hash using your webhook key
3. Encode the hash as Base64
4. Compare with the `x-revolv3-signature` header

### Verification Examples

#### C# Example

```csharp theme={null}
public static bool IsValidWebhookEventSignature(
    string requestBody,
    string signatureHeader,
    string signatureKey,
    string url)
{
    if (string.IsNullOrEmpty(signatureKey))
        throw new ArgumentNullException(nameof(signatureKey));
    if (string.IsNullOrEmpty(url))
        throw new ArgumentNullException(nameof(url));
    
    // Concatenate url + "$" + requestBody
    byte[] bytes = Encoding.UTF8.GetBytes(url + "$" + requestBody);
    
    // Create HMAC-SHA256 hash
    using var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(signatureKey));
    
    // Compare signatures
    return Convert.ToBase64String(hmacsha256.ComputeHash(bytes)).Equals(signatureHeader);
}
```

#### PHP Example

```php theme={null}
function IsValidWebhookEventSignature(
    string $requestBody,
    string $signatureHeader,
    string $signatureKey,
    string $url
): bool {
    if (empty($signatureKey)) {
        throw new ArgumentNullException('signatureKey');
    }
    if (empty($url)) {
        throw new ArgumentNullException('url');
    }

    // Concatenate url + "$" + requestBody
    $data = $url . '$' . $requestBody;

    // Create HMAC-SHA256 hash
    $hmac = hash_hmac('sha256', $data, $signatureKey, true);

    // Compare signatures
    return base64_encode($hmac) === $signatureHeader;
}
```

#### JavaScript/Node.js Example

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

function isValidWebhookSignature(requestBody, signatureHeader, signatureKey, url) {
  if (!signatureKey || !url) {
    throw new Error('signatureKey and url are required');
  }
  
  // Concatenate url + "$" + requestBody
  const data = url + '$' + requestBody;
  
  // Create HMAC-SHA256 hash
  const hmac = crypto.createHmac('sha256', signatureKey);
  hmac.update(data);
  const hash = hmac.digest('base64');
  
  // Compare signatures (use constant-time comparison for security)
  return crypto.timingSafeEqual(
    Buffer.from(hash),
    Buffer.from(signatureHeader)
  );
}
```

## Best Practices for Webhook Handling

### 1. Always Verify Signatures

**Never process webhooks without verifying the signature first.** This is a critical security requirement.

### 2. Respond Quickly

* **Respond with 200 OK** as soon as you receive the webhook
* **Process asynchronously** if processing takes time
* **Don't block** on long operations

### 3. Handle Duplicates

Webhooks may be sent multiple times. Make your handler **idempotent**:

* Check if you've already processed this event
* Use event IDs or timestamps to detect duplicates
* Don't process the same event twice

### 4. Log Everything

* Log all incoming webhooks (for debugging)
* Store event data for auditing
* Track which events you've processed

### 5. Handle Errors Gracefully

* If processing fails, log the error
* Consider retrying failed processing
* Don't crash your server on webhook errors

## Common Webhook Scenarios

### Scenario 1: Payment Succeeds

**Event**: `InvoiceStatusChanged` **Status**: `Paid`

**What to do**:

1. Verify signature
2. Parse the Body JSON
3. Extract invoice ID and status
4. Update order status to "paid"
5. Fulfill the order (ship product, activate service, etc.)
6. Send confirmation email to customer
7. Respond with 200 OK

### Scenario 2: ACH Payment Completes

**Event**: `InvoiceStatusChanged` **Status**: `Paid` (after 1-5 business days)

**What to do**:

1. Verify signature
2. Parse the Body JSON
3. Check if status is "Paid" or "Noncollectable"
4. If Paid: Fulfill order, send confirmation
5. If Noncollectable: Notify customer, ask for different payment method
6. Respond with 200 OK

### Scenario 3: Subscription Payment Fails

**Event**: `InvoiceAttemptStatusChanged` **Status**: `Fail`

**What to do**:

1. Verify signature
2. Parse the Body JSON
3. Check the failure reason
4. Notify customer that payment failed
5. Pause subscription (optional)
6. Give customer a grace period to update payment method
7. Respond with 200 OK

## Troubleshooting

### Webhook Not Received

**Check**:

* Is your endpoint publicly accessible? (webhooks can't reach localhost)
* Is your endpoint returning 200 OK?
* Are you checking the correct URL?
* Is your server firewall blocking requests?

### Signature Verification Fails

**Check**:

* Are you using the correct webhook key?
* Are you concatenating URL + "\$" + body correctly?
* Are you using HMAC-SHA256 and Base64 encoding?
* Is the URL exactly as configured (including https\://)?

### Duplicate Events

**Solution**:

* Implement idempotency checking
* Store event IDs or timestamps
* Skip processing if already handled

## Next Steps

* [**Creating a Webhook**](/docs/webhook-events/creating-a-webhook) — Step-by-step guide to setting up webhooks
* [**Invoice Status**](/docs/core-concepts/invoice/invoice-status) — Understand invoice statuses you'll receive in webhooks
* [**Subscriptions**](/docs/core-concepts/subscription/create-subscription) — Learn about subscription events

***
