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

# Webhook

## 1. Overview

When business state changes occur, EasyBilling sends webhook notifications as HTTP `POST` requests with a JSON body.

### 1.1 Supported Event Types

| Event Type (`type`)                   | Description                                                        |
| ------------------------------------- | ------------------------------------------------------------------ |
| `payment.succeeded`                   | Invoice payment succeeds (auto-charge or manual payment)           |
| `credit_schedule.depleted`            | Prepaid credits are depleted (balance reaches zero)                |
| `invoice.posted`/`credit_memo.posted` | Invoice  or credit memo is posted                                  |
| `contract.created`                    | New customer contract becomes effective                            |
| `contract.updated`                    | Contract change takes effect: update/switch-plan/early-renew/renew |
| `contract.cancelled`                  | Contract is cancelled/terminated                                   |

### 1.2 Standard Payload Structure

All webhook events are wrapped in a common envelope:

* `id`: Globally unique event ID (`UUID v4`)
* `type`: Event type
* `createdAt`: Event creation timestamp
* `data.object`: Event business payload

The `id` remains unchanged across retries and should be used as your idempotency key for deduplication.

#### Example: `payment.succeeded`

```json theme={null}
{
  "id": "0e41a324-0da2-4ba4-a3e4-c5126c81a05c",
  "type": "payment.succeeded",
  "createdAt": "2026-07-05T11:04:02Z",
  "data": {
    "object": {
      "basicInfo": {
        "contractId": "144fc477-0c79-4908-84d8-7501a06caf73",
        "accountNumber": "SM-ACC-00000001",
        "accountId": "c780170e-3737-4ec7-91d3-972c660820e9"
      },
      "businessInfo": {
        "Payment": {
          "externalReferenceId": "Order-123-upgrade",
          "invoiceId": "fa73013c-1bd9-421b-8fbb-bfb6bbf37343",
          "paymentStatus": "succeeded",
          "invoiceNumber": "INV-000000121",
          "currency": "USD",
          "totalAmount": "99.00"
        }
      }
    }
  }
}
```

#### Example: `credit_schedule.depleted`

```json theme={null}
{
    "id": "1aba9755-d5e6-43b4-9a86-b2294e69f42d",
    "type": "credit_schedule.depleted",
    "createdAt": "2026-07-27T07:54:01Z",
    "data": {
        "object": {
            "basicInfo": {
                "contractId": "b492c672-890f-4fd8-aab6-ab36c72900f4",
                "accountId": "f3978fb9-2d3a-43cc-a49e-7151ab6a15b8",
                "accountNumber": "SM-ACC-00000001",
                "contractSegmentId": "9cb1b340-7eaa-4d66-b4ae-261ab3fa9611"
            },
            "businessInfo": {
                "CreditSchedule": {
                    "totalBalance": "20000.000000000",
                    "remainingBalance": "0.000000000",
                    "bucketId": "75f35768-a110-422b-9352-847ec3504a41",
                    "creditScheduleId": "aabe90c9-91a0-4359-882f-cd64c6aa02d5",
                    "validFrom": "2026-07-01",
                    "validTo": "2026-08-01",
                    "originalTotalBalance": "20000.000000000",
                    "uom": "Token",
                    "bucketType": "resource-based-credit"
                }
            }
        }
    }
}
```

#### Example: `invoice.posted, credit_memo.posted`

```json theme={null}
{
  "id": "771e8400-f29b-41d4-b716-556655441111",
  "type": "invoice.posted",
  "createdAt": "2026-08-03T11:00:00Z",
  "data": {
    "object": {
      "basicInfo": {
        "accountId": "f3978fb9-2d3a-43cc-a49e-7151ab6a15b8",
        "accountNumber": "SM-ACC-00000001",
        "documentId": "fa73013c-1bd9-421b-8fbb-bfb6bbf37343",
        "documentNumber": "INV-000000121"
      },
      "businessInfo": {
        "billingDocument": {
          "amount": "299.00",
          "totalAmount": "316.94",
          "discountAmount": "0.00",
          "taxAmount": "17.94",
          "currency": "USD",
          "postStatus": "succeeded"
        }
      }
    }
  }
}
```

#### Example: `contract.created, contract.updated, contract.cancelled`

```json theme={null}
{
  "id": "882e8400-a29b-41d4-c716-666655442222",
  "type": "contract.created",
  "createdAt": "2026-08-03T11:15:00Z",
  "data": {
    "object": {
      "basicInfo": {
        "accountId": "f3978fb9-2d3a-43cc-a49e-7151ab6a15b8",
        "accountNumber": "SM-ACC-00000001",
        "contractId": "b492c672-890f-4fd8-aab6-ab36c72900f4"
      },
      "businessInfo": {
        "ContractAction": {
          "contractActionType": "create-contract-with-plan",
          "contractInfo": {
            "contractNumber": "CT-0000000085",
            "effectiveDate": "2026-08-01",
            "expirationDate": "2027-07-31",
            "status": "active",
            "paymentGatewayType": "stripe-connect",
            "contractSegments": [
              {
                "id": "9cb1b340-7eaa-4d66-b4ae-261ab3fa9611",
                "effectiveDate": "2026-08-01",
                "expirationDate": "2027-07-31",
                "planId": "d3e40eb2-c5d4-4141-b9d2-c8ebdad3c542"
              }
            ]
          }
        }
      }
    }
  }
}
```

## 2. Signature Verification (HMAC-SHA256)

To prevent tampering and spoofing, each webhook request includes this header:

* Header name: `X-Webhook-Signature`
* Header format: `t=<unix_timestamp_seconds>,v1=<hex_digest>`
* String to sign: `timestamp + "." + rawBody`
* Algorithm: `HMAC-SHA256` with your webhook secret

### Critical Validation Rule

Always compute the signature using the **raw HTTP request body bytes**. Do not re-serialize parsed JSON before verification, or signature checks may fail due to formatting/key-order differences.

### Python Example

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

def verify_webhook_signature(raw_body: str, signature_header: str, secret: str, tolerance_seconds=300) -> bool:
    if not signature_header or not secret:
        return False

    parts = dict(part.split('=', 1) for part in signature_header.split(','))
    timestamp_str = parts.get('t')
    provided_sig = parts.get('v1')

    if not timestamp_str or not provided_sig:
        return False

    try:
        timestamp = int(timestamp_str)
    except ValueError:
        return False

    # 1) Replay protection via timestamp tolerance window
    if abs(time.time() - timestamp) > tolerance_seconds:
        return False

    # 2) Recompute HMAC-SHA256
    to_sign = f"{timestamp}.{raw_body}".encode("utf-8")
    expected_sig = hmac.new(secret.encode("utf-8"), to_sign, hashlib.sha256).hexdigest()

    # 3) Constant-time comparison to avoid timing attacks
    return hmac.compare_digest(expected_sig, provided_sig)
```

## 3. Retry Policy and Time Window

### Timestamp Tolerance

* Recommended tolerance: `300` seconds (5 minutes)
* If request timestamp is outside the tolerance window, reject the request
* This helps prevent replay attacks

### Delivery Retry Policy

EasyBilling automatically retries webhook delivery when:

* Receiver returns non-2xx HTTP status
* Network timeout/failure occurs

Defaults and behavior:

* Default max retries: `3`
* Retry intervals: 1 minute
* Retry with the same webhook id
* If all retries fail, event status is marked as `FAILED`

## 5. Receiver Best Practices

1. Return `2xx` quickly after minimal validation, then process asynchronously.
2. Use webhook `id` for deduplication across retries.
3. Verify signature before any business processing.
4. Enforce timestamp tolerance (recommended: 5 minutes).
5. Log key metadata (`id`, `type`, `createdAt`, delivery timestamp, verification result).
6. Implement safe retry handling in your own downstream processing.

## 6. Go-Live Checklist

* [ ] Webhook endpoint is reachable from Billing
* [ ] Signature verification is implemented with raw body
* [ ] Replay protection window is enabled
* [ ] Event deduplication by webhook `id` is implemented
* [ ] Non-2xx handling and observability are in place
* [ ] Sandbox event triggering is tested end-to-end
