# Guide to Ascend Webhooks: Features and Best Practices

Ascend webhooks notify your application about payment activity: an invoice is paid, a payout is sent, a refund is processed. If you're building on Ascend for insurance payments or premium financing, webhooks are how you react to these events without polling.

This guide covers how Ascend webhooks work, the events you'll handle, how to verify the `X-Ascend-Signature`, and the best practices for production.

## What are Ascend webhooks?

Ascend webhooks are JSON POSTs delivered over HTTPS to a URL you register with Ascend. Each is signed with an `X-Ascend-Signature` header formatted `t=<timestamp>,v1=<hex>`. The signature is an HMAC-SHA256 (hex) over `<timestamp> + ":" + raw_body`, keyed with your webhook secret. The same timestamp also arrives in the `X-Ascend-Request-Timestamp` header. This is a custom scheme, not Svix or Standard Webhooks, so don't reach for a Standard Webhooks library.

## Ascend webhook features

| Feature | Details |
| --- | --- |
| Configuration | Manual: email Ascend support with your org, environment, events, and URL (no self-serve dashboard) |
| Signature header | `X-Ascend-Signature`, formatted `t=<ts>,v1=<hex>` |
| Signature scheme | HMAC-SHA256 (hex) over `<timestamp>:<raw_body>`, keyed with the webhook secret |
| Timestamp | Also sent in `X-Ascend-Request-Timestamp` |
| Payload | `{ id, type, data }` |
| Acknowledgement | Return HTTP 200 |
| Retry / tolerance | Not documented |
| SDK | None |

## Common events

Ascend event names follow a `noun.verb` pattern. One spelling detail is worth pinning: refunds use the British `cancelled` while payouts use the American `canceled`.

| Event | Fires when |
| --- | --- |
| `invoice.paid` | An invoice is paid |
| `payout.paid` | A payout is paid |
| `payout.canceled` | A payout is canceled (American spelling) |
| `refund.cancelled` | A refund is cancelled (British spelling) |

Match the exact spelling per event rather than assuming one convention across the board.

## Setting up Ascend webhooks

Registration is manual. Email Ascend support with your organization, environment, the events you want, and your endpoint URL. There's no self-serve dashboard, so keep a record of what you registered. Ascend provides the webhook secret you use for verification.

## Securing Ascend webhooks

The `X-Ascend-Signature` header is `t=<ts>,v1=<hex>`. To verify, build `<timestamp> + ":" + raw_body`, compute an HMAC-SHA256 with your webhook secret, hex-encode it, and compare against the `v1` value. Verify against the raw body, re-serializing the JSON changes the bytes and breaks the match.

```javascript
const crypto = require("crypto");

const SECRET = process.env.ASCEND_WEBHOOK_SECRET;

function verify(rawBody, signatureHeader) {
  const params = Object.fromEntries(
    (signatureHeader || "").split(",").map((kv) => kv.split("="))
  );
  const timestamp = params.t;
  const message = `${timestamp}:${rawBody.toString()}`;
  const expected = crypto.createHmac("sha256", SECRET).update(message).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(params.v1 || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-ascend-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body)); // branch on type, async
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SECRET = os.environ["ASCEND_WEBHOOK_SECRET"].encode()

def verify(raw_body: bytes, signature_header: str) -> bool:
    params = dict(
        kv.split("=", 1) for kv in (signature_header or "").split(",") if "=" in kv
    )
    timestamp = params.get("t", "")
    message = (timestamp + ":").encode() + raw_body
    expected = hmac.new(SECRET, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, params.get("v1", ""))

```

## Ascend webhook limitations and pain points

### The signed message is `timestamp:body`, and it's not Standard Webhooks

The Problem: The HMAC is over `<timestamp> + ":" + raw_body`, and the header format is Stripe-like but the scheme is Ascend's own. Reaching for a Svix or Standard Webhooks verifier fails, and so does signing the body alone.

Why It Happens: Ascend binds the timestamp into the signed message with a colon separator and ships a custom format.

Workarounds:

* Build `timestamp + ":" + body`, HMAC-SHA256, hex, and compare against the `v1` value over the raw body.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge with the right scheme, so your app receives pre-verified events without hand-rolling the format.

### The spelling differs per event

The Problem: Refunds use `cancelled` (British) and payouts use `canceled` (American). A handler keyed to one spelling silently misses the other.

Why It Happens: The event catalog isn't spelled consistently.

Workarounds:

* Use the exact per-event spelling, and handle unknown types defensively.

How Hookdeck Can Help: Hookdeck routes on the exact `type` value you receive, so a spelling mismatch never drops an event.

### Registration is manual

The Problem: There's no dashboard. You register by email, so it's easy to lose track of which events and URLs are live.

Why It Happens: Ascend onboards webhooks through support.

Workarounds:

* Keep your own record of registered events and endpoints, and re-confirm after changes.

How Hookdeck Can Help: Hookdeck gives you a single managed endpoint and a record of every event that arrives, so you can see what's actually being delivered.

### Retry and tolerance are undocumented

The Problem: Ascend doesn't document retry behavior or a timestamp tolerance, so you can't rely on assumptions about redelivery or replay windows.

Why It Happens: The behavior isn't published.

Workarounds:

* Make handlers idempotent, and enforce your own freshness window on the timestamp.

How Hookdeck Can Help: Hookdeck retries with its own policy and records every attempt, so delivery doesn't depend on undocumented provider behavior.

## Best practices

### Verify over `timestamp:body` with the webhook secret

Build `<timestamp>:<raw_body>`, HMAC-SHA256 with your secret, hex, and compare against `v1` in constant time.

### Enforce your own freshness window

Ascend doesn't document a tolerance, so compare `X-Ascend-Request-Timestamp` against your own window to guard against replay.

### Return 200 and process asynchronously

Acknowledge quickly and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Retry behavior is undocumented, so dedupe on the payload `id` and make processing safe to repeat.

## Conclusion

Ascend webhooks are verified with an `X-Ascend-Signature` HMAC-SHA256 over `<timestamp>:<raw_body>`, keyed with your webhook secret, with the timestamp echoed in `X-Ascend-Request-Timestamp`. It's a custom scheme, not Standard Webhooks. Verify over the raw body, enforce your own freshness window, match event spellings exactly, and make handlers idempotent.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Ascend webhooks reliably in minutes.