Skip to content
Try Free →

Custom webhook skill recipes

Last updated: · 5 min read

Recipe 1: Zendesk ticket creation

When the bot escalates, create a Zendesk ticket:

app.post('/askvault-webhook', async (req, res) => {
const { conversation, trigger } = req.body;
if (trigger !== 'escalate_to_human') return res.json({ok: true});
await fetch('https://yourco.zendesk.com/api/v2/tickets.json', {
method: 'POST',
headers: {Authorization: 'Basic ...'},
body: JSON.stringify({
ticket: {
subject: conversation.summary,
comment: { body: conversation.transcript },
requester: { email: conversation.customer.email },
priority: conversation.priority,
}
})
});
res.json({ok: true, ticket_created: true});
});

Recipe 2: HubSpot lead from chat

When collect_lead succeeds:

@app.post('/askvault-webhook')
def webhook(req):
if req.json['event'] != 'lead.captured': return {'ok': True}
lead = req.json['data']
requests.post(
'https://api.hubapi.com/crm/v3/objects/contacts',
headers={'Authorization': f'Bearer {HS_TOKEN}'},
json={'properties': {
'email': lead['email'],
'firstname': lead.get('name','').split()[0],
'lead_source': 'AskVault Chat',
'lead_score': lead['score']
}}
)
return {'ok': True}

Recipe 3: Stripe refund

When the bot processes a refund:

app.post('/askvault-refund', async (req, res) => {
const { customer_id, amount } = req.body;
const refund = await stripe.refunds.create({
payment_intent: req.body.payment_intent,
amount: amount * 100,
});
res.json({ok: true, refund_id: refund.id});
});

The bot's policy bounds (per-visitor cap, per-workspace cap) verified before this webhook fires.

Recipe 4: Custom CRM sync

For proprietary CRM systems:

@app.post('/askvault-webhook')
def webhook(req):
event = req.json
if event['event'] == 'conversation.resolved':
# Sync to your CRM
crm_post('/api/conversation', {
'customer_id': event['data']['customer']['id'],
'transcript': event['data']['transcript'],
'resolution': event['data']['resolution_note'],
'duration': event['data']['duration_seconds']
})
return {'ok': True}

Recipe 5: Discord ping on hot lead

app.post('/askvault-webhook', async (req, res) => {
const event = req.body;
if (event.event === 'lead.qualified' && event.data.score > 80) {
await fetch(DISCORD_WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify({
content: `:fire: Hot lead: ${event.data.name} from ${event.data.company}`,
}),
headers: {'Content-Type': 'application/json'},
});
}
res.json({ok: true});
});

Common patterns

All recipes follow:

  • Filter by event type. Don't react to irrelevant events.
  • Idempotency. Use delivery_id to avoid duplicates.
  • Signature verification. HMAC-SHA256 with shared secret.
  • 2xx response to AskVault, even on internal error.

Configuring the skill

  1. AI Agents > Skills > custom_webhook > Configure.
  2. Add webhook URL for each event.
  3. Copy signing secret.
  4. Test with sample event.

Signature verification

import hmac, hashlib
def verify_signature(body, signature_header, secret):
parts = dict(p.split('=') for p in signature_header.split(','))
expected = hmac.new(
secret.encode(),
f'{parts["t"]}.{body}'.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts['v1'])

Use in every recipe before processing.

Limits

  • Webhook timeout. 10 seconds.
  • Payload size. 256 KB.
  • Retry attempts. 3 with exponential backoff.
  • Custom webhooks per workspace. Up to 25 on Business, unlimited Enterprise.

Common pitfalls

Endpoint timeout. Optimize or use async processing.

Signature mismatch. Wrong secret or wrong algorithm. Use HMAC-SHA256.

Duplicate processing. Implement idempotency.

Returning 5xx by accident. AskVault retries. Return 200 even on internal errors; log internally.

FAQ

Can the bot wait for the webhook response?

Yes if the webhook is fast (under 5 seconds). For long-running, fire-and-forget.

Will the bot retry on my 5xx?

Yes up to 3 times.

Can I use multiple custom webhooks?

Yes; one per use case is recommended.

Was this page helpful?