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.
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:
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 thev1value 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.
Make handlers idempotent
Retry behavior is undocumented, so dedupe on the payload id and make processing safe to repeat.
Make Ascend webhooks production-ready
Hookdeck verifies X-Ascend-Signature, deduplicates, and durably queues every event
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 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 for free and handle Ascend webhooks reliably in minutes.