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

# Webhooks

> Receive real-time notifications for events in your TRXN account.

Webhooks allow your application to receive real-time HTTP notifications when events occur in your TRXN account. Instead of polling the API for changes, you register a URL and TRXN sends a POST request to that URL whenever a relevant event fires.

## How webhooks work

<Steps>
  <Step title="Create an endpoint">
    Register a webhook endpoint URL and choose which events to subscribe to. You can subscribe to specific events or use `["*"]` to receive all events.

    See the [webhook endpoints API reference](/api-reference/webhook-endpoints) for CRUD operations.
  </Step>

  <Step title="Store the signing secret">
    When you create an endpoint, TRXN returns a signing secret (format: `whsec_<64_hex_chars>`). Store this securely -- it is only shown in full once, at creation time.
  </Step>

  <Step title="Receive events">
    When a subscribed event occurs, TRXN sends an HTTP POST request to your endpoint with a JSON payload describing the event.
  </Step>

  <Step title="Verify and process">
    Your server verifies the request signature, returns a 2xx response, then processes the event asynchronously.
  </Step>
</Steps>

## Payload format

Every webhook delivery sends a JSON body with this structure:

```json theme={null}
{
  "event": "invoice.created",
  "payload": {
    "id": "inv_xxx",
    "object": "invoice",
    "customer_id": "cus_xxx",
    "status": "pending",
    "total_amount": "100.00"
  },
  "timestamp": "2025-01-14T12:00:00Z"
}
```

## Signature verification

TRXN signs every webhook request with HMAC-SHA256 so you can verify that the request genuinely came from TRXN.

### Signature header format

Every webhook request includes an `X-Trxn-Signature` header:

```
X-Trxn-Signature: t=<unix_timestamp>,v1=<signature>
```

**Example:**

```
X-Trxn-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

### Verification steps

1. Extract the timestamp (`t`) and signature (`v1`) from the header
2. Check that the timestamp is within 5 minutes of the current time (prevents replay attacks)
3. Compute the expected signature: `HMAC-SHA256(secret, "<timestamp>.<raw_body>")`
4. Compare signatures using constant-time comparison

<Note>
  Use the full signing secret including the `whsec_` prefix as the HMAC key. This matches Stripe's implementation pattern.
</Note>

### Code examples

<CodeGroup>
  ```ruby Ruby theme={null}
  def verify_webhook(payload, signature_header, secret)
    parts = signature_header.split(",").to_h { |p| p.split("=", 2) }
    timestamp = parts["t"].to_i
    signature = parts["v1"]

    # Check timestamp is within 5 minutes
    return false if (Time.now.to_i - timestamp).abs > 300

    # Compute expected signature (use full secret including whsec_ prefix)
    signed_payload = "#{timestamp}.#{payload}"
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)

    # Constant-time comparison
    ActiveSupport::SecurityUtils.secure_compare(expected, signature)
  end
  ```

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

  function verifyWebhook(payload, signatureHeader, secret) {
    const parts = Object.fromEntries(
      signatureHeader.split(',').map(p => p.split('='))
    );
    const timestamp = parseInt(parts.t, 10);
    const signature = parts.v1;

    // Check timestamp is within 5 minutes
    if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
      return false;
    }

    // Compute expected signature (use full secret including whsec_ prefix)
    const signedPayload = `${timestamp}.${payload}`;
    const expected = crypto
      .createHmac('sha256', secret)
      .update(signedPayload)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }
  ```

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

  def verify_webhook(payload: str, signature_header: str, secret: str) -> bool:
      parts = dict(p.split("=", 1) for p in signature_header.split(","))
      timestamp = int(parts["t"])
      signature = parts["v1"]

      # Check timestamp is within 5 minutes
      if abs(time.time() - timestamp) > 300:
          return False

      # Compute expected signature (use full secret including whsec_ prefix)
      signed_payload = f"{timestamp}.{payload}"
      expected = hmac.new(
          secret.encode(),
          signed_payload.encode(),
          hashlib.sha256
      ).hexdigest()

      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

## Available events

### Customer events

| Event              | Description                     |
| ------------------ | ------------------------------- |
| `customer.created` | A new customer has been created |
| `customer.updated` | A customer has been updated     |
| `customer.deleted` | A customer has been deleted     |

### Product events

| Event             | Description                    |
| ----------------- | ------------------------------ |
| `product.created` | A new product has been created |
| `product.updated` | A product has been updated     |
| `product.deleted` | A product has been deleted     |

### Price events

| Event           | Description                  |
| --------------- | ---------------------------- |
| `price.created` | A new price has been created |
| `price.updated` | A price has been updated     |
| `price.deleted` | A price has been deleted     |

### Invoice events

| Event             | Description                        |
| ----------------- | ---------------------------------- |
| `invoice.created` | A new invoice has been created     |
| `invoice.updated` | An invoice has been updated        |
| `invoice.paid`    | An invoice has been marked as paid |
| `invoice.overdue` | An invoice has become overdue      |

### Subscription events

| Event                    | Description                         |
| ------------------------ | ----------------------------------- |
| `subscription.created`   | A new subscription has been created |
| `subscription.activated` | A subscription has become active    |
| `subscription.canceled`  | A subscription has been canceled    |
| `subscription.renewed`   | A subscription has been renewed     |

### Subscription phase events

| Event                        | Description                               |
| ---------------------------- | ----------------------------------------- |
| `subscription_phase.created` | A new subscription phase has been created |
| `subscription_phase.updated` | A subscription phase has been updated     |
| `subscription_phase.deleted` | A subscription phase has been deleted     |

**Payload example:**

```json theme={null}
{
  "event": "subscription_phase.created",
  "payload": {
    "id": "sub_phase_xxx",
    "object": "subscription_phase",
    "subscription_id": "sub_xxx",
    "start_date": "2025-02-01T00:00:00Z",
    "end_date": "2025-03-01T00:00:00Z",
    "items": [
      {
        "id": 123,
        "price_id": "price_xxx",
        "quantity": 1,
        "overridden_price_amount": null
      }
    ],
    "created_at": "2025-01-26T12:00:00Z",
    "updated_at": "2025-01-26T12:00:00Z"
  },
  "timestamp": "2025-01-26T12:00:00Z"
}
```

### Wallet events

| Event            | Description                   |
| ---------------- | ----------------------------- |
| `wallet.created` | A new wallet has been created |

### Crypto address events

| Event                    | Description                                     |
| ------------------------ | ----------------------------------------------- |
| `crypto_address.created` | A new crypto address has been added to a wallet |
| `crypto_address.deleted` | A crypto address has been deleted               |

### Payment claim events

| Event                     | Description                              |
| ------------------------- | ---------------------------------------- |
| `payment_claim.submitted` | A customer has submitted a payment claim |
| `payment_claim.approved`  | A payment claim has been approved        |
| `payment_claim.rejected`  | A payment claim has been rejected        |

### Crypto transaction events

| Event                          | Description                                    |
| ------------------------------ | ---------------------------------------------- |
| `crypto_transaction.received`  | A crypto transaction has been received         |
| `crypto_transaction.allocated` | A transaction has been allocated to an invoice |

### Transaction allocation events

| Event                            | Description                                   |
| -------------------------------- | --------------------------------------------- |
| `transaction_allocation.created` | A new transaction allocation has been created |

## Response handling

Your endpoint must respond with a 2xx status code to indicate successful receipt. If your endpoint returns a non-2xx status code, TRXN retries with exponential backoff for up to 11 attempts.

### Error handling

| Error type       | Behavior                                                  |
| ---------------- | --------------------------------------------------------- |
| Connection error | Endpoint is disabled to prevent further delivery attempts |
| Timeout error    | Retry with exponential backoff                            |
| TLS error        | Retry with exponential backoff                            |
| Non-2xx response | Retry with exponential backoff                            |

## Event retention

TRXN retains webhook events for **30 days**.

## Best practices

<Tip>
  Return a 2xx response **before** any complex processing. Process the event asynchronously after responding to avoid timeouts.
</Tip>

1. **Respond quickly** -- return a 2xx response before any complex logic that could cause a timeout. Process the event asynchronously after responding.
2. **Handle duplicates** -- webhook endpoints may occasionally receive the same event more than once. Make your event processing idempotent by tracking the `id` from the payload.
3. **Handle out-of-order delivery** -- events may not arrive in chronological order. Use the `timestamp` field to determine the sequence rather than assuming arrival order.
4. **Always verify signatures** -- use the `X-Trxn-Signature` header to verify all incoming webhooks. Reject any webhook that fails signature verification.
5. **Use HTTPS** -- always use HTTPS endpoints for security.
6. **Store secrets securely** -- store your signing secrets in environment variables or a secrets manager, never in source code.
7. **Monitor failures** -- check webhook events in the dashboard to monitor delivery status.
8. **Regenerate compromised secrets** -- if you suspect your signing secret has been compromised, regenerate it immediately via the API.

## Managing webhook endpoints

For creating, updating, deleting, and regenerating secrets for webhook endpoints, see the [webhook endpoints API reference](/api-reference/webhook-endpoints).
