Skip to content
Try Free →

Webhook recipes - send AskVault events to your tools

Last updated: · 5 min read

Two ways to connect your systems

AskVault can reach your code in two different ways, and they are easy to mix up.

  • Webhooks tell your endpoint that something happened: lead.captured, conversation.escalated, conversation.resolved and more. Deliveries to your own endpoints are signed; deliveries to Slack or Discord incoming-webhook URLs are reformatted for that service and not signed. Set them up on the Webhooks page (see the webhooks reference for the full event list).
  • The custom_webhook skill is an action. The bot calls your endpoint in the middle of a conversation with arguments it collected, and uses your reply to answer. It authenticates with a header you choose, not a signature. Business plan and above. Business+

Verify the signature first

Every delivery to your own endpoint carries these headers:

HeaderWhat it holds
X-AskVault-Signaturesha256= followed by the hex HMAC
X-AskVault-TimestampUnix time the delivery was signed
X-AskVault-DeliveryA unique id; use it to ignore repeats
X-AskVault-EventThe event name

The HMAC is computed over the timestamp, a dot, then the raw body bytes. Parse the JSON only after the check passes.

import hmac, hashlib
def verify(raw_body: bytes, secret: str, sig_header: str, ts_header: str) -> bool:
expected = hmac.new(
secret.encode(), f"{ts_header}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", sig_header)
import crypto from 'node:crypto';
function verify(rawBody, secret, sigHeader, tsHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${tsHeader}.`)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(sigHeader || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

In Express, read the body with express.raw({ type: 'application/json' }) so you sign the exact bytes AskVault sent.

Recipe 1: post new leads to Discord

The quickest way needs no code: paste a Discord (or Slack) incoming-webhook URL as an endpoint on the Webhooks page, and AskVault formats each event as a message for that service. Use a relay like the one below only when you want your own message format.

app.post('/askvault', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verify(req.body, process.env.ASKVAULT_SECRET,
req.get('X-AskVault-Signature'), req.get('X-AskVault-Timestamp'))) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
if (event.event === 'lead.captured') {
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: `New lead: ${event.data.name || event.data.email}` }),
});
}
res.json({ ok: true });
});

The lead.captured payload's data holds lead_id, email, name and source. Leads the bot captured (source: ai_skill) also carry conversation_id; leads from the widget's lead form (source: widget) carry phone and page_url instead.

Recipe 2: open a ticket when a chat is escalated

from flask import Flask, request, abort
import os, requests
app = Flask(__name__)
seen = set() # use your database in production
@app.post("/askvault")
def askvault():
if not verify(request.get_data(), os.environ["ASKVAULT_SECRET"],
request.headers.get("X-AskVault-Signature", ""),
request.headers.get("X-AskVault-Timestamp", "")):
abort(401)
delivery = request.headers.get("X-AskVault-Delivery")
if delivery in seen:
return {"ok": True}
seen.add(delivery)
event = request.get_json()
if event["event"] == "conversation.escalated":
data = event["data"]
requests.post(os.environ["TICKETS_URL"], json={
"subject": "Chat needs a person",
"body": f"Conversation {data['conversation_id']} on {data.get('channel_type', 'chat')}",
"external_id": data["conversation_id"],
}, timeout=5)
return {"ok": True}

Swap TICKETS_URL for your help desk's ticket API. Zendesk and Freshdesk both accept a subject, body and external id.

Recipe 3: let the bot check stock in your own system

This one uses the custom_webhook skill, so the bot calls you and waits for the answer.

  1. On the Skills page, open custom_webhook and set the endpoint URL and a request schema, for example a single sku string. Calls are sent as POST; choosing another method is rolling out.
  2. Add an auth header, such as Authorization: Bearer <token>. The value is stored encrypted.
  3. Return JSON the bot can read:
app.post('/stock', express.json(), async (req, res) => {
if (req.get('Authorization') !== `Bearer ${process.env.STOCK_TOKEN}`) {
return res.status(401).end();
}
const { sku } = req.body;
const qty = await inventory.count(sku);
res.json({ sku, in_stock: qty > 0, quantity: qty });
});

The skill waits up to 15 seconds for a reply. A 5xx or timeout is retried (twice by default); a 4xx is not. Keep the endpoint fast, because the visitor is waiting.

Limits

  • Webhook deliveries. 10 second timeout per attempt, up to 3 attempts on 5xx, timeout or connection failure, no retry on 4xx.
  • custom_webhook skill. 15 second timeout, 2 retries by default on 5xx or timeout.
  • Custom actions. Up to 10 per workspace.

Common pitfalls

Every signature check fails. You are probably hashing parsed JSON or leaving out the timestamp. Hash timestamp + "." + raw body.

The same event arrives twice. Retries happen. Store X-AskVault-Delivery and skip ids you have seen.

Your endpoint returns 4xx by mistake. AskVault treats it as final and stops retrying. Return 2xx once you have accepted the event, and handle your own errors internally.

FAQ

Can the bot wait for the webhook response?

Only with the custom_webhook skill, which waits up to 15 seconds. Event webhooks are notifications; the conversation does not wait for them.

Do webhooks count against my message allowance?

No. Messages are counted on AI replies, not on webhook deliveries.

Which plan do I need?

Webhooks need Growth or above. The custom_webhook skill needs Business or above.

Was this page helpful?