Webhook Signing and Verification
Verify CatFee Webhook authenticity and integrity with per-user HMAC-SHA256 signatures, including User Center settings, integration examples, replay protection, and idempotency.
When webhook signing is enabled, CatFee adds an HMAC-SHA256 signature to your HTTP/HTTPS callbacks. Your server verifies the signature with your secret to confirm that the sender holds the same secret and that the request body and event headers have not been modified.
Each user has one secret and one switch, shared across all of that user's HTTP/HTTPS callbacks. Signing is disabled by default. The secret is generated the first time you enable signing, retained when you disable it, and reused when you enable it again. Telegram and email notifications are unaffected.
Quick Start
Prepare your receiver using an example below: read the raw request body, verify the signature, and then parse JSON.
Enable webhook signing in the User Center, copy your signing secret (referred to below as
secret), and store it in your receiver's environment variables or secret store.Check your implementation with the fixed test vector below, then send a test notification to verify a real callback.
Add persistent idempotency and business processing. Return
HTTP 200with the bodysuccessonly after reliable acceptance.
Enabling signing takes effect immediately for subsequent sends, including retries already queued. Coordinate enabling signing, configuring the secret, and deploying verification code. During this transition, return a non-2xx status if verification fails so the platform can retry under its existing policy; do not temporarily accept unsigned requests or invalid signatures.
Enable, View, and Disable Signing
Manage signing and view your secret in the User Center.
Enable Signing and Get Your Secret
Sign in to the User Center and open the webhook signing settings.
Enable signing. On first activation, the system generates a signing secret for your user.
Copy the secret to your receiver's environment variables or secret store, for example
CATFEE_WEBHOOK_SECRET.
Throughout this guide and the verification code, secret means the signing secret copied from the User Center: 32 random bytes encoded with standard Base64. It is different from the API Secret used for the energy purchase API. Do not put it in frontend code, logs, or public repositories.
View Current Settings
View the current signing status and secret in the User Center's webhook signing settings. No secret exists before the first activation. After that, disabling signing retains the secret.
Disable Signing
Turn off signing in the User Center. Subsequent HTTP/HTTPS callbacks will no longer include signatures. Enabling signing again reuses the same secret.
The switch applies to all HTTP/HTTPS callbacks for the current user and is checked at each actual send, including test notifications and retries of existing notifications. Update your receiver's acceptance policy when disabling signing.
Headers and Signature Rules
When signing is enabled, two headers are added alongside the existing event headers:
X-EVENT-ID
Event ID, used for signing and idempotency; unchanged across retries
X-EVENT-TYPE
Event type, such as EVENT_DELEGATION
X-EVENT-VERSION
Business event protocol version, currently 20260930
X-EVENT-TIMESTAMP
New: Unix timestamp in seconds for this send, as a decimal string
X-EVENT-SIGNATURE
New: v1= followed by the 64-character lowercase hexadecimal HMAC-SHA256 result
Header names are case-insensitive. Do not modify the values used in signing. No key ID is required, and the secret must not be included in the callback request.
Calculation
Decode the
secretcopied from the User Center with standard Base64 to obtain the 32-byte key. Do not use the Base64 string itself as the HMAC key.Build the prefix in the following order, then append the raw request body bytes:
Compare your calculated signature with the received signature using a constant-time comparison. The examples use
timingSafeEqual,compare_digest, andMessageDigest.isEqual, respectively.
The separator \n is one LF byte (0x0a), not a backslash followed by n, and not CRLF. Prefix fields contain non-empty printable ASCII values. The final prefix field is followed by one LF; do not append a newline after the body.
Receiver Examples
Select your language, save the code as described below, and configure CATFEE_WEBHOOK_SECRET. The examples check signature format, enforce a 300-second time window, and compare signatures in constant time. You must implement persistent idempotency and reliable business processing separately. Code is split into two file tabs; use each code block’s copy button to save it.
This example uses CommonJS. Save the two code tabs as verify.js and receiver.js in the same directory. Register the callback route before global express.json() middleware so it verifies the raw body before parsing JSON.
Save the two code tabs as verify.py and receiver.py in the same directory. Read the raw bytes with await request.body() and parse JSON only after verification succeeds. Do not serialize the result of await request.json() for verification.
Save the two code tabs as VerifyWebhook.java and CallbackController.java in the same package, adding the corresponding package declarations. Java 17 or later is required. Use byte[] for @RequestBody to verify the raw bytes before parsing business objects.
These controllers demonstrate signature verification. Implement business processing or reliable queuing at the TODO comments before deploying them to production. If one callback endpoint serves multiple CatFee users, select the user's secret using a configured route or other trusted context; do not trust a user identity claimed by an unverified body.
Replay Protection, Idempotency, and Retries
A valid signature does not mean you have never received the notification before. Your receiver also needs:
A time window: Accept requests whose timestamp differs from the current time by no more than 300 seconds, rejecting both stale requests and timestamps too far in the future. The examples implement this check. Keep your server clock synchronized.
Persistent idempotency: Atomically deduplicate by user,
X-EVENT-TYPE, andX-EVENT-ID. Mark an event complete only after the business transaction succeeds or the message is reliably queued. Allow retries after processing failures; an in-memorySetis insufficient for production.Duplicate handling: Return
HTTP 200withsuccessfor events already processed successfully, without repeating their business effects.Verification on every retry: The platform generates a timestamp and signature for each send; the event ID remains unchanged. Use
X-EVENT-TIMESTAMP, not the business event time in the body. Identical requests sent within the same second may have identical signatures.
Return a non-2xx status when verification fails. After verification and reliable acceptance, return HTTP 200 with the body success. Signed callbacks do not automatically follow 3xx redirects, so configure the final receiver URL directly.
Verify with a Fixed Test Vector
Save the following content as test-vector.json in the same directory as your verification source. These values are for local testing only and must not be used for production callbacks:
The raw body is the following single line of UTF-8 text, with no trailing newline. Keep the Chinese text unchanged: it exercises Unicode handling and is part of the signed bytes.
Run the self-test for your chosen language:
The self-tests fix the current time at 1790726400 and check the valid signature, body or header tampering, an incorrect secret, and the time window. Production receivers must use the actual current time, never the fixed test time.
After the self-test, send a test notification for a configured subscription from the User Center to verify a real callback. Test notifications use the same signing switch and secret. Balance, subscription, and delegation events are currently supported; test notifications are not added to the retry queue.
Troubleshooting
Verification always fails
Use the webhook secret and decode it with standard Base64 first; do not use the API Secret for the energy purchase API
Fields match but the signature differs
Check for JSON reserialization, extra whitespace, Unicode escaping, or a trailing newline; verify the raw bytes
Event IDs do not match
Sign with the X-EVENT-ID header, not the JSON event_id, which may include a D- or B- prefix
Timestamp validation fails
Use Unix seconds, not milliseconds or an ISO date; check your server clock and do not use the business event time
Old events arrive after enabling signing
The switch applies at send time, including pending retries; verify against this send's timestamp
Does disabling and enabling signing change the secret?
No. The same user secret is reused
Is HTTP supported?
Yes, with the same signature rules as HTTPS; use HTTPS when transport encryption is required
Callbacks fail when the URL redirects
Signed callbacks do not follow redirects; configure the final receiver URL directly
Return to Webhook for event structures and business fields.
Last updated
Was this helpful?