This document describes the webhook system for receiving real-time notifications about vault and yield events.
The following events are supported:
deposit- User deposits funds into a vaultwithdraw- User withdraws funds from a vaultyield_distributed- Yield is distributed for an epochvault_state_changed- Vault transitions between lifecycle statesvault.matured- Vault reaches maturity datevault_created- New vault is deployed
All webhooks use a common envelope structure:
{
"event": "deposit",
"contractId": "CBQHNAXSI55GX2GN6D67GK7BHVPSLJUGZQEU7WJ5LKR5PNUCGLIMAO4K",
"timestamp": 1719417600000,
"payload": {
"user": "GBXXL...",
"amount": "1000000000",
"shares": "1000000000"
}
}{
"event": "deposit",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {
"user": "GBXXL...",
"amount": "1000000000",
"shares": "1000000000"
}
}{
"event": "withdraw",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {
"user": "GBXXL...",
"amount": "500000000",
"shares": "500000000"
}
}{
"event": "yield_distributed",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {
"epoch": 1,
"total_yield": "50000000",
"total_shares": "10000000000"
}
}{
"event": "vault_state_changed",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {
"new_state": "Active"
}
}{
"event": "vault.matured",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {}
}{
"event": "vault_created",
"contractId": "CBQHN...",
"timestamp": 1719417600000,
"payload": {
"asset_address": "CDLZFC...",
"admin": "GBXXL...",
"expected_apy": 500,
"maturity_date": 1735689600,
"funding_deadline": 1720022400,
"min_deposit": "100000000",
"max_deposit": "10000000000"
}
}All webhook requests include an HMAC-SHA256 signature in the X-StellarYield-Signature header. Verify this signature to ensure the webhook is authentic.
- Extract the signature from the
X-StellarYield-Signatureheader - Compute HMAC-SHA256 of the raw request body using your webhook secret
- Compare the computed signature with the received signature using constant-time comparison
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-stellaryield-signature'];
const payload = JSON.stringify(req.body);
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhook(payload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
console.log('Received event:', req.body.event);
res.status(200).json({ received: true });
});import hmac
import hashlib
def verify_webhook(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-StellarYield-Signature')
payload = request.get_data(as_text=True)
secret = os.environ.get('WEBHOOK_SECRET')
if not verify_webhook(payload, signature, secret):
return {'error': 'Invalid signature'}, 401
event = request.json
print(f"Received event: {event['event']}")
return {'received': True}, 200- Always verify the signature before processing webhook events
- Use constant-time comparison to prevent timing attacks
- Respond with HTTP 200 within 5 seconds to prevent retries
- Process webhooks asynchronously to avoid blocking the response
- Store webhook secrets securely in environment variables
- Log all webhook events for debugging and audit purposes