Webhooks
Webhooks allow FLUX to notify your application in real time when important events occur, such as successful filings, failures, or status updates. Instead of polling the API, your system receives HTTP callbacks as events happen.
How Webhooks Work
- 1You register a webhook URL in your application.
- 2FLUX sends an HTTP POST request to that URL when an event occurs.
- 3Your server validates the request and processes the event.
- 4You return a 2xx response to acknowledge receipt.
When to Use Webhooks
Use webhooks when:
- You need real-time updates on filing status
- You want to track success or failure without polling
- You are building asynchronous or event-driven workflows
Why We Secure Webhooks
Authenticity (Proving Who Sent the Request)
Because webhook receivers are publicly accessible HTTP endpoints on the internet (e.g., https://your-api.com/api/webhooks/taxstreem), anyone on the web who discovers that URL can send POST requests to it.
Without a secret, an attacker could send fake POST requests to your receiver containing malicious or forged data (e.g., fake transaction tax treatments or fake filing completions). The webhook secret proves the request genuinely originated from TaxStreem.
Data Integrity (Preventing Tampering)
When TaxStreem dispatches an event payload, it creates a cryptographic signature using the secret key (typically using HMAC-SHA512):
TaxStreem sends this signature in the X-Webhook-Signature header. When your endpoint receives the request:
- Your server computes
HMAC-SHA512(ReceivedBody, YourSecretKey). - Your server compares its computed signature against the
X-Webhook-Signatureheader. - If they match exactly, you know the payload was not altered or tampered with in transit.
Preventing Spoofing & Financial / Business Risks
In tax, billing, and accounting systems like TaxStreem:
- A fake webhook could trick a developer's system into marking unclassified or illegal transactions as tax-compliant.
- A fake filing completion webhook could mark unpaid VAT/CIT filings as paid in the developer's ERP/CRM.
Validating the signature using the webhook secret ensures that automated system workflows (database updates, status transitions, notifications) are only triggered by verified, authentic events.
Supported Events
TaxStreem emits webhook events for key lifecycle changes across all products:
vat.filing.success{
"eventType": "vat.filing.success",
"eventPayload": {
"scheduleSheet": "https://storage.googleapis.com/bucket/partner123/schedule.xlsx",
"filingReport": "https://storage.googleapis.com/bucket/partner123/report.png",
"filingId": "fil_abc123",
"partnerId": "partner_456",
"status": true,
"taxType": "VAT",
"paymentDetails": {
"paymentReference": "PAY-9023490",
"paymentURL": "https://pay.taxstreem.com/PAY-9023490",
"paymentAmount": "15000",
"taxType": "VAT"
}
}
}vat.filing.no-pending{
"eventType": "vat.filing.no-pending",
"eventPayload": {
"filingId": "fil_abc123",
"status": false,
"message": "No Pending Filing for this period",
"error_type": "",
"details": "No Pending Filing for this period"
}
}vat.filing.already-filed{
"eventType": "vat.filing.already-filed",
"eventPayload": {
"filingId": "fil_abc123",
"status": false,
"message": "Already filed",
"error_type": "",
"details": "Already filed"
}
}vat.filing.failed{
"eventType": "vat.filing.failed",
"eventPayload": {
"filingId": "fil_abc123",
"status": false,
"message": "TaxID validation failed",
"error_type": "VAT_FILING_ERROR",
"details": "Taxpayer TaxID does not exist in NRS"
}
}wht.filing.success{
"eventType": "wht.filing.success",
"eventPayload": {
"scheduleSheet": "https://storage.googleapis.com/bucket/partner123/schedule.xlsx",
"filingReport": "https://storage.googleapis.com/bucket/partner123/report.png",
"filingId": "fil_abc123",
"partnerId": "partner_456",
"status": true,
"taxType": "WHT",
"paymentDetails": {
"paymentReference": "PAY-9023490",
"paymentURL": "https://pay.taxstreem.com/PAY-9023490",
"paymentAmount": "15000",
"taxType": "WHT"
}
}
}wht.filing.failed{
"event": "wht.filing.failed",
"data": {
"filingId": "fil_wht002",
"status": "failed",
"error": "Rate validation failed",
"beneficiary": "Vendor Inc"
}
}Webhook Request
All webhook events are delivered as HTTP POST requests with a JSON payload.
Headers
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-Webhook-Signature | HMAC SHA512 signature generated with your webhook secret key to verify request authenticity. |
| X-Event-Type | The event type identifier (e.g., developer.numen.result, vat.filing.success). |
Verify Event Origin
Since your webhook URL is publicly available, you need to verify that events originate from TaxStreem and not a bad actor. There are two ways to ensure events to your webhook URL are from TaxStreem:
- Signature validation (Recommended)
- IP whitelisting
Signature Validation
Events sent from TaxStreem carry the X-Webhook-Signature header. The value of this header is an HMAC SHA512 signature of the event payload signed using your secret key.
โ ๏ธ Verifying the header signature should be done before processing the event.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hash = crypto
.createHmac('sha512', secret)
.update(JSON.stringify(payload))
.digest('hex');
return hash === signature;
}
// Usage in your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'] || req.headers['x-taxstreem-signature'];
const isValid = verifyWebhookSignature(req.body, signature, process.env.TAXSTREEM_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process the event
res.status(200).send('OK');
});import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
hash_obj = hmac.new(
secret.encode('utf-8'),
json.dumps(payload).encode('utf-8'),
hashlib.sha512
)
return hash_obj.hexdigest() == signature
# Usage in Flask
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature') or request.headers.get('x-taxstreem-signature')
is_valid = verify_webhook_signature(
request.json,
signature,
os.environ['TAXSTREEM_SECRET']
)
if not is_valid:
return 'Invalid signature', 401
# Process the event
return 'OK', 200package main
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"encoding/json"
)
func verifyWebhookSignature(payload interface{}, signature, secret string) bool {
payloadBytes, _ := json.Marshal(payload)
h := hmac.New(sha512.New, []byte(secret))
h.Write(payloadBytes)
computedHash := hex.EncodeToString(h.Sum(nil))
return computedHash == signature
}
// Usage in HTTP handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Webhook-Signature")
if signature == "" {
signature = r.Header.Get("x-taxstreem-signature")
}
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
if !verifyWebhookSignature(payload, signature, os.Getenv("TAXSTREEM_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the event
w.WriteHeader(http.StatusOK)
}import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WebhookVerifier {
private static final ObjectMapper mapper = new ObjectMapper();
public static boolean verifyWebhookSignature(Object payload, String signature, String secret) throws Exception {
String payloadJson = mapper.writeValueAsString(payload);
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
byte[] hash = mac.doFinal(payloadJson.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString().equals(signature);
}
}
// Usage in Spring Boot controller
@RestController
public class WebhookController {
@PostMapping("/webhook")
public ResponseEntity<String> handleWebhook(
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
@RequestBody Map<String, Object> payload) throws Exception {
String secret = System.getenv("TAXSTREEM_SECRET");
if (!WebhookVerifier.verifyWebhookSignature(payload, signature, secret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
}
// Process the event
return ResponseEntity.ok("OK");
}
}Security Best Practices
- Always validate the signature before processing the payload
- Reject requests with invalid or missing signatures (return 401)
- Use HTTPS endpoints only
- Store your secret key securely (environment variables, secrets manager)
- Never log the secret key or signature in plain text
Retry & Failure Handling
If your endpoint fails to respond with a 2xx status code, FLUX will retry delivery automatically following an exponential backoff strategy.
Webhook Response
Your server should respond with any 2xx HTTP status code to acknowledge successful receipt.