Reward Distribution Webhook Service
TaskOn provides a secure webhook service to notify project parties when user rewards have been distributed. This service ensures that your application can respond promptly to completed tasks and reward distributions with proper authentication and user context.
Overview
When a user receives a reward after task completion, TaskOn will make an authenticated HTTP POST request to your configured webhook endpoint to notify your project of the reward distribution. This allows you to update your internal systems, trigger additional workflows, or provide personalized user notifications.
Important Notes:
- The webhook is only triggered when users actually receive rewards
- Some tasks may not immediately distribute rewards upon submission (e.g., Proof of Work tasks requiring manual review)
- All webhook requests include proper authentication headers and user information
- TaskOn will retry failed requests up to 3 times, so ensure your endpoint handles idempotent requests
Webhook Configuration
Project parties must provide a webhook endpoint URL that can receive reward distribution notifications. TaskOn will make HTTP POST requests to this endpoint when rewards are distributed.
Authentication
All webhook requests are authenticated using API keys and HMAC signatures to ensure security and data integrity.
Authentication Headers
TaskOn includes the following authentication headers in webhook requests:
X-API-Key: Your unique API key identifierX-Signature: HMAC-SHA256 signature for request verification
Signature Verification
The signature is generated using HMAC-SHA256 with your secret key and a payload containing user_id and timestamp:
{
"user_id": 11111,
"timestamp": 1758251308
}2
3
4
Authentication Implementation Examples
Important Note: For signature verification to work correctly across different implementations, it's crucial that both TaskOn and your project use identical JSON serialization formats. The signature is generated using a JSON payload with user_id and timestamp fields in alphabetical order without extra spaces.
Go Implementation (Server Side)
Signature Generation (TaskOn Implementation):
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
)
func main() {
secretKey := "your-secret-key"
h := hmac.New(sha256.New, []byte(secretKey))
// Create payload with fixed key order to match Node.js JSON.stringify
payload := map[string]interface{}{
"user_id": 11111,
"timestamp": 1758251308,
}
// Use consistent JSON formatting (no spaces, sorted keys)
data, err := json.Marshal(payload)
if err != nil {
return
}
h.Write(data)
signature := hex.EncodeToString(h.Sum(nil))
fmt.Println("X-Signature:", signature)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Signature Verification (Project Side):
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
signature := r.Header.Get("X-Signature")
expectedApiKey := "your-expected-api-key"
secretKey := "your-secret-key"
// Verify API key
if apiKey != expectedApiKey {
http.Error(w, "Invalid API key", http.StatusUnauthorized)
return
}
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Parse payload to extract user_id and timestamp
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Create signature payload with consistent key order
signaturePayload := map[string]interface{}{
"user_id": payload["user_id"],
"timestamp": payload["timestamp"],
}
// Generate expected signature with consistent JSON formatting
h := hmac.New(sha256.New, []byte(secretKey))
signatureData, err := json.Marshal(signaturePayload)
if err != nil {
http.Error(w, "JSON marshal error", http.StatusInternalServerError)
return
}
h.Write(signatureData)
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Verify signature
if signature != expectedSignature {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook payload
// ... your business logic here ...
// Return success response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"result": true,
"error": nil,
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Node.js Implementation (JavaScript)
const crypto = require("crypto");
const express = require("express");
function verifyWebhookSignature(payload, signature, secretKey) {
// Create signature payload with consistent key order (matches Go implementation)
const signaturePayload = {
user_id: payload.user_id,
timestamp: payload.timestamp,
};
// Use consistent JSON formatting - JavaScript naturally sorts keys alphabetically
const expectedSignature = crypto
.createHmac("sha256", secretKey)
.update(JSON.stringify(signaturePayload))
.digest("hex");
return signature === expectedSignature;
}
app.post("/webhook/reward-notification", express.json(), (req, res) => {
const apiKey = req.headers["x-api-key"];
const signature = req.headers["x-signature"];
const expectedApiKey = process.env.EXPECTED_API_KEY;
const secretKey = process.env.SECRET_KEY;
// Verify API key
if (apiKey !== expectedApiKey) {
return res.status(401).json({
result: false,
error: "Invalid API key",
});
}
// Verify signature
if (!verifyWebhookSignature(req.body, signature, secretKey)) {
return res.status(401).json({
result: false,
error: "Invalid signature",
});
}
// Process webhook payload
const {
task_id,
reward_type,
reward_amount,
user_id,
evm_address,
email,
timestamp,
} = req.body;
console.log("Reward distributed:", {
task_id,
reward_type,
reward_amount,
user_id,
email,
timestamp,
});
// Your business logic here...
res.json({
result: true,
error: null,
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Retry Policy
- Retry Attempts: If the webhook call fails, TaskOn will retry up to 3 times
- Idempotency: Please ensure your webhook endpoint handles idempotent requests properly, as the same notification may be sent multiple times due to retries
- Authentication: All retry attempts include the same authentication headers
Webhook Payload
The webhook payload includes comprehensive information about the reward distribution and user context. The structure varies depending on the reward type configured for the task.
Common Fields
All webhook payloads include the following user and context information:
user_id: Unique identifier of the user who completed the taskevm_address: Array of EVM wallet addresses associated with the useremail: User's email addresstimestamp: Unix timestamp when the reward was distributed
You can import the TypeScript interface from the SDK:
import { WebhookPayload } from "@taskon/embed";For detailed type definitions, see WebhookPayload.
Token Rewards
When the task reward is configured as a token, the webhook payload includes the following fields:
{
"task_id": 111111,
"reward_type": "Token",
"reward_amount": "0.1",
"token_contract": "0x0000...",
"token_symbol": "USDT",
"token_network": "bsc",
"user_id": 11111,
"evm_address": ["0xaaaaaaaaa", "0xbbbbbbbbb"],
"email": "user@example.com",
"timestamp": 1758251308
}2
3
4
5
6
7
8
9
10
11
12
Field Descriptions:
task_id: Unique identifier of the completed taskreward_type: Always "Token" for token rewardsreward_amount: The numeric amount of tokens distributedtoken_contract: Contract address of the distributed tokentoken_symbol: Symbol of the distributed tokentoken_network: The blockchain network where the token was distributeduser_id: Unique identifier of the user who received the rewardevm_address: Array of EVM wallet addresses associated with the useremail: User's email addresstimestamp: Unix timestamp when the reward was distributed
Points Rewards
When the task reward is configured as points, the webhook payload includes the following fields:
{
"task_id": 222222,
"reward_type": "GTCPoints",
"reward_amount": "100",
"point_name": "PointNameA",
"user_id": 11111,
"evm_address": ["0xaaaaaaaaa", "0xbbbbbbbbb"],
"email": "user@example.com",
"timestamp": 1758251308
}2
3
4
5
6
7
8
9
10
Field Descriptions:
task_id: Unique identifier of the completed taskreward_type: Always "GTCPoints" for points rewardsreward_amount: The numeric amount of points distributedpoint_name: The name of the point system useduser_id: Unique identifier of the user who received the rewardevm_address: Array of EVM wallet addresses associated with the useremail: User's email addresstimestamp: Unix timestamp when the reward was distributed
Reward Types
The reward_type field can have the following values:
| Value | Description |
|---|---|
Token | Cryptocurrency or token reward |
GTCPoints | Points-based reward system |
Expected Response
Your webhook endpoint should return a JSON response indicating successful processing:
{
"result": true,
"error": null
}2
3
4
Response Fields:
result: Boolean indicating whether the webhook was processed successfullyerror: Should benullfor successful processing, or contain error details if processing failed
Complete Implementation Example
Here's a complete webhook endpoint implementation with authentication:
const crypto = require("crypto");
const express = require("express");
const app = express();
app.use(express.json());
function verifyWebhookSignature(payload, signature, secretKey) {
const signaturePayload = {
user_id: payload.user_id,
timestamp: payload.timestamp,
};
const expectedSignature = crypto
.createHmac("sha256", secretKey)
.update(JSON.stringify(signaturePayload))
.digest("hex");
return signature === expectedSignature;
}
app.post("/webhook/reward-notification", (req, res) => {
try {
const apiKey = req.headers["x-api-key"];
const signature = req.headers["x-signature"];
const expectedApiKey = process.env.EXPECTED_API_KEY;
const secretKey = process.env.SECRET_KEY;
// Verify API key
if (apiKey !== expectedApiKey) {
return res.status(401).json({
result: false,
error: "Invalid API key",
});
}
// Verify signature
if (!verifyWebhookSignature(req.body, signature, secretKey)) {
return res.status(401).json({
result: false,
error: "Invalid signature",
});
}
// Extract webhook data
const {
task_id,
reward_type,
reward_amount,
token_contract,
token_symbol,
token_network,
point_name,
user_id,
evm_address,
email,
timestamp,
} = req.body;
console.log("Reward notification received:", {
task_id,
reward_type,
reward_amount,
user_id,
email,
timestamp,
});
// Process the webhook based on reward type
if (reward_type === "Token") {
console.log(
`User ${email} received ${reward_amount} ${token_symbol} tokens`
);
// Handle token reward logic
} else if (reward_type === "GTCPoints") {
console.log(
`User ${email} received ${reward_amount} ${point_name} points`
);
// Handle points reward logic
}
// Update your internal systems here
// ... your business logic ...
// Return success response
res.json({
result: true,
error: null,
});
} catch (error) {
console.error("Webhook processing error:", error);
res.status(500).json({
result: false,
error: error.message,
});
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
Testing Your Webhook Endpoint
You can test your webhook endpoint using the following curl commands with proper authentication:
Generate Test Signature
First, generate a test signature for your test payload:
# Example using Node.js to generate signature
node -e "
const crypto = require('crypto');
const secretKey = 'your-secret-key';
const payload = { user_id: 11111, timestamp: 1758251308 };
const signature = crypto.createHmac('sha256', secretKey)
.update(JSON.stringify(payload))
.digest('hex');
console.log('Test signature:', signature);
"2
3
4
5
6
7
8
9
10
Testing Token Reward Webhook
curl --location 'https://your-api-domain.com/webhook/reward-notification' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: your-test-api-key' \
--header 'X-Signature: your-generated-signature' \
--data '{
"task_id": 111111,
"reward_type": "Token",
"reward_amount": "0.1",
"token_contract": "0x0000...",
"token_symbol": "USDT",
"token_network": "bsc",
"user_id": 11111,
"evm_address": ["0xaaaaaaaaa", "0xbbbbbbbbb"],
"email": "test@example.com",
"timestamp": 1758251308
}'2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Testing Points Reward Webhook
curl --location 'https://your-api-domain.com/webhook/reward-notification' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: your-test-api-key' \
--header 'X-Signature: your-generated-signature' \
--data '{
"task_id": 222222,
"reward_type": "GTCPoints",
"reward_amount": "100",
"point_name": "PointNameA",
"user_id": 11111,
"evm_address": ["0xaaaaaaaaa", "0xbbbbbbbbb"],
"email": "test@example.com",
"timestamp": 1758251308
}'2
3
4
5
6
7
8
9
10
11
12
13
14
Testing Invalid Authentication
Test your authentication by sending requests with invalid credentials:
# Test with invalid API key
curl --location 'https://your-api-domain.com/webhook/reward-notification' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: invalid-key' \
--header 'X-Signature: your-generated-signature' \
--data '{...}'
# Test with invalid signature
curl --location 'https://your-api-domain.com/webhook/reward-notification' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: your-test-api-key' \
--header 'X-Signature: invalid-signature' \
--data '{...}'2
3
4
5
6
7
8
9
10
11
12
13
Security Best Practices
Authentication Security
- API Key Storage: Store your API keys and secret keys securely using environment variables or secure key management systems
- Signature Verification: Always verify both API key and signature for every webhook request
- Timing Attack Prevention: Use constant-time comparison for signature verification to prevent timing attacks
Implementation Best Practices
- Idempotency: Implement proper idempotency handling since TaskOn may retry failed webhook calls
- Timeout Handling: Ensure your webhook endpoint responds within a reasonable timeframe (recommended: under 30 seconds)
- Error Logging: Log all webhook requests and responses for debugging and monitoring
- Graceful Error Handling: Return appropriate HTTP status codes and error messages
- User Privacy: Handle user information (email, addresses) in compliance with privacy regulations
- Async Processing: For complex operations, consider processing webhooks asynchronously to ensure quick response times
Rate Limiting and Monitoring
- Rate Limiting: Implement rate limiting to protect against potential abuse
- Monitoring: Set up monitoring and alerting for webhook failures
- Logging: Log authentication failures and successful webhook processing for audit purposes
Troubleshooting
Authentication Issues
401 Unauthorized - Invalid API Key:
- Verify your API key matches the expected value
- Check that the API key is being sent in the
X-API-Keyheader - Ensure there are no extra spaces or characters in the API key
401 Unauthorized - Invalid Signature:
- Verify your secret key is correct
- Ensure signature generation uses the exact same payload structure:
{"user_id": <id>, "timestamp": <timestamp>} - Check JSON serialization format (no extra spaces, consistent ordering)
- Verify HMAC-SHA256 algorithm is being used correctly
Missing Authentication Headers:
- Ensure both
X-API-KeyandX-Signatureheaders are present - Header names are case-sensitive
- Ensure both
Common Issues
Webhook Not Received:
- Verify your endpoint URL is accessible and returns the expected response format
- Check if authentication is properly implemented
- Ensure your server accepts POST requests
Multiple Notifications:
- This is expected behavior due to retry logic
- Implement proper idempotency using task_id and timestamp
- Handle duplicate notifications gracefully
Timeout Errors:
- Optimize webhook processing to respond quickly (under 30 seconds)
- Move heavy processing to background jobs
- Return success response immediately after validation
Signature Verification Fails:
- Double-check the JSON format used for signature generation
- Verify the secret key is identical on both sides
- Ensure consistent JSON serialization (use
JSON.stringifywithout formatting)
Debugging Tips
- Log Everything: Log incoming headers, payload, and generated signatures for comparison
- Test Locally: Use tools like ngrok to test webhooks locally during development
- Validate Signatures: Create a separate endpoint to test signature generation and verification
- Check Timestamps: Ensure timestamp values match between signature generation and verification
Support
If you encounter issues with webhook integration, please contact TaskOn support with:
- Your webhook endpoint URL
- Sample request/response logs including headers
- Authentication configuration details (without exposing sensitive keys)
- Error messages or unexpected behavior descriptions
- Signature verification code samples