Custom webhook skill recipes
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_idto avoid duplicates. - Signature verification. HMAC-SHA256 with shared secret.
- 2xx response to AskVault, even on internal error.
Configuring the skill
- AI Agents > Skills > custom_webhook > Configure.
- Add webhook URL for each event.
- Copy signing secret.
- 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.