Webhooks
Subscribing to Topics
When creating a webhook, you can specify which topics (event types) you want to subscribe to. You can either:
- Subscribe to specific topics: Provide an array of specific topic names (e.g.,
["PaymentIntentCreated", "PaymentCreated"]) - Subscribe to all topics: Use the wildcard
"*"to subscribe to all current and future topics
When you use "*" to subscribe to all topics, all new topics added in the future will automatically be sent to your webhook. This means you are responsible for handling any new or unknown topic types that may be introduced. Make sure your webhook handler can gracefully handle topics it doesn't recognize.
Note: The "*" wildcard cannot be combined with other topics. If you use "*", it must be the only element in the topics array.
Security
We sign each webhook request so you can verify that it is coming from our server. In order to verify signatures you need to calculate a signature using the request body and your webhook secret and then compare it to the signature that is included in the request headers.
The secret is only available at the time the webhook is created. It cannot be accessed after that time. If you lose the secret, you will need to delete and re-create a new webhook.
To verify the signature:
- Retrieve the JSON payload (request body) as a string
- Compute the SHA256 hash and compute HMAC (hex string) with your signing secret
- Compare it against
x-accrue-signatureheader value
Below are some code samples to calculate the signature:
- secret - the signing secret provided when the webhook is created
- payload - the webhook request body (JSON payload) as a string
- signature - the webhook signature provided in the header
x-accrue-signature
- node.js
- nest.js
- php
- python
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const port = 3000;
const webhookSecret = process.env.WEBHOOK_SECRET;
app.use(bodyParser.json());
app.post('/webhook', (req, res) => {
const signature = req.headers['x-accrue-signature'];
const body = JSON.stringify(req.body);
const computedHmac = crypto.createHmac('sha256', webhookSecret).update(body).digest('hex');
if (computedHmac === signature) {
console.log('Signatures match');
res.status(200).send('OK');
} else {
console.log('Signatures do not match');
res.status(403).send('Forbidden');
}
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { createHmac } from 'crypto';
@Injectable()
export class AccrueWebhookAuthGuard implements CanActivate {
public canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
const signature = request.header('X-Accrue-Signature');
if (!signature) {
return false;
}
const webhookSecret = process.env.WEBHOOK_SECRET;
const computedHmac = createHmac('sha256', webhookSecret).update(JSON.stringify(request.body), 'utf-8').digest('hex');
return computedHmac === signature;
}
}
$secret = 'your_secret_key';
function calculateHmac($secret, $body) {
return hash_hmac('sha256', $body, $secret);
}
$body = file_get_contents('php://input');
$headers = getallheaders();
$signature = isset($headers['X-Accrue-Signature']) ? $headers['X-Accrue-Signature'] : '';
$hmac = calculateHmac($secret, $body);
if (hash_equals($hmac, $signature)) {
http_response_code(200);
echo 'OK';
} else {
http_response_code(403);
echo 'Forbidden';
}
from flask import Flask, request, abort
import hmac
import hashlib
app = Flask(__name__)
secret = b'your_secret_key'
def calculate_hmac(secret, body):
return hmac.new(secret, body, hashlib.sha256).hexdigest()
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Accrue-Signature', '')
body = request.get_data()
hmac_calculated = calculate_hmac(secret, body)
if hmac.compare_digest(hmac_calculated, signature):
return 'OK', 200
else:
abort(403)
if __name__ == '__main__':
app.run(port=3000)