Skip to main content

Webhooks

Overview

Webhooks are the primary way the Feature Platform communicates asynchronously with your system. After you submit an activity—either through the Tracking SDK or the server-side API—our platform processes it in the background. Once a final status is reached (e.g., a digital asset is successfully issued or an error occurs), we will send a notification to your registered webhook URL.

Implementing a secure and robust webhook handler is a critical step in the integration process, as described in the Integration Guide.

Security & Signature Verification

To ensure that your system only processes requests genuinely originating from the Feature Platform, every webhook request is signed. We include a signature and a timestamp in the headers of each request, which you must verify using your Client Secret.

Webhook Headers

HeaderDescription
x-feature-signatureThe HMAC-SHA256 signature of the request payload and timestamp, encoded as a hexadecimal string.
x-feature-timestampThe UTC timestamp (in milliseconds) when the request was sent. Used to prevent replay attacks.
Content-Typeapplication/json

Verifying the Signature

The signature is a HMAC-SHA256 hash created from the raw request body concatenated with the timestamp from the x-feature-timestamp header, using your Client Secret as the key.

Steps to Verify:

  1. Extract the x-feature-signature and x-feature-timestamp from the request headers.
  2. Prepare the dataToSign string by concatenating the raw request body (as a string) with the timestamp.
  3. Calculate the expected signature by generating a HMAC-SHA256 hash of dataToSign using your Client Secret.
  4. Compare the signature from the header with your expected signature using a timing-safe comparison method.
  5. If the signatures match and the timestamp is recent, you can trust the webhook. Otherwise, you must reject the request.

Code Example (Node.js / TypeScript)

Here is a function to verify the signature in a Node.js environment:

import * as crypto from 'crypto';

/**
* Verifies a webhook signature from the Feature Platform.
* @param rawBody The raw, unparsed request body string.
* @param signature The signature from the 'x-feature-signature' header.
* @param timestamp The timestamp from the 'x-feature-timestamp' header.
* @param clientSecret Your tenant's Client Secret.
* @returns True if the signature is valid, false otherwise.
*/
function verifyWebhookSignature(
rawBody: string,
signature: string,
timestamp: string,
clientSecret: string
): boolean {
const dataToSign = rawBody + timestamp;
const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(dataToSign)
.digest('hex');

// Use timingSafeEqual to prevent timing attacks
try {
return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature));
} catch {
return false; // Catches errors if buffers have different lengths
}
}

Code Example (Python)

Here is the equivalent verification function in Python:

import hmac
import hashlib
import time


def verify_webhook_signature(raw_body: str, signature: str, timestamp: str, client_secret: str) -> bool:
"""
Verifies a webhook signature from the Feature Platform.
"""
data_to_sign = f"{raw_body}{timestamp}"
expected_signature = hmac.new(
client_secret.encode('utf-8'),
data_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()

# Use hmac.compare_digest for timing-safe comparison
return hmac.compare_digest(expected_signature, signature)

Security Best Practices

  • Validate the Timestamp: Always check the x-feature-timestamp and reject requests that are too old (e.g., more than 5 minutes). This prevents replay attacks.
  • Use HTTPS: Your webhook endpoint URL must use HTTPS to ensure data is encrypted in transit.
  • Store Secrets Securely: Never expose your Client Secret in client-side code or commit it to version control. Use a secrets manager.

Event Types & Payloads

All webhook payloads share a common structure. The event field determines the type of notification.

Common Payload Structure

{
"event": "event.name",
"activityId": "unique-id-of-the-original-activity",
"userId": "the-user-id-you-provided",
"tenantId": "your-tenant-id",
"data": {
...
}
// Event-specific data
}

Event: activity.succeeded

Sent when an activity has been successfully processed and has resulted in a digital asset being issued. This is the primary success event.

Payload Example:

{
"event": "activity.succeeded",
"activityId": "act_123456789",
"userId": "user-xyz-987",
"tenantId": "tenant_abc123",
"data": {
"completedAt": "2025-11-14T20:30:00Z",
"transactionHash": "0xabc...def",
"blockchain": "avalanche-fuji",
"digitalAsset": {
"contractAddress": "0x123...456",
"tokenId": "42",
"metadataUrl": "https://meta.feature.io/assets/42"
}
}
}

Key data Fields:

  • completedAt: The ISO 8601 timestamp of when the issuance was confirmed on-chain.
  • transactionHash: The unique hash of the blockchain transaction.
  • digitalAsset: An object containing details of the asset that was minted.

Event: activity.failed

Sent when an activity could not be processed successfully due to a validation error, a blockchain issue, or other internal problem.

Payload Example:

{
"event": "activity.failed",
"activityId": "act_987654321",
"userId": "user-abc-123",
"tenantId": "tenant_abc123",
"data": {
"failedAt": "2025-11-14T21:00:00Z",
"error": {
"code": "insufficient_funds",
"message": "The designated minting wallet has insufficient funds to complete the transaction."
}
}
}

Key data Fields:

  • failedAt: The ISO 8601 timestamp of the failure.
  • error: An object containing a machine-readable code and a human-readable message.

Anatomy of a Webhook Request

When the Feature Platform sends a webhook to your server, it looks like this. Note the custom security headers and the JSON body.

Incoming HTTP Request (POST):

POST /your-webhook-endpoint HTTP/1.1
Host: your-api.com
Content-Type: application/json
User-Agent: Feature-Platform-Webhook/1.0
x-feature-signature: 5b50d80c7dc7ae8bb1acc562290a7b0e32...
x-feature-timestamp: 1678901234567

{
"event": "activity.succeeded",
"activityId": "act_550e8400-e29b",
"userId": "user_123",
"tenantId": "tenant_abc",
"data": {
"completedAt": "2025-03-15T10:30:00Z",
"transactionHash": "0x88df01e...",
"digitalAsset": {
"contractAddress": "0x123abc...",
"tokenId": "42",
"metadataUrl": "https://meta.feature.io/assets/42",
"status": "minted"
}
}
}

Your Expected Response:

Your server must acknowledge receipt immediately to prevent retries.

  • Response Code: 200 OK
  • Response Body: (Optional, but { "received": true } is standard practice).
// Example Express.js handler response
res.status(200).json({received: true});

Implementation Best Practices

  1. Respond Quickly: Your endpoint should return a 200 OK status code as quickly as possible. Acknowledge receipt of the webhook before doing any complex processing.
  2. Process Asynchronously: Offload the webhook payload to a queue (like RabbitMQ, SQS, or Pub/Sub) and process it with a background worker. This prevents timeouts and allows you to handle spikes in traffic gracefully.
  3. Handle Retries with Idempotency: The Feature Platform may occasionally send the same webhook more than once. Design your handler to be idempotent, meaning it can safely process the same event multiple times without creating duplicate data or side effects. Use the activityId as an idempotency key.
  4. Log Everything: Log all incoming webhooks and the results of their processing. This will be invaluable for troubleshooting.