Overview
Instead of constantly polling our API for updates, webhooks deliver event notifications instantly when something happens:- Transaction status changes
- Customer onboarding updates
- Account changes
- System events
Setting Up Webhooks
1. Create a Webhook Endpoint
Create an endpoint in your application to receive webhook notifications:JavaScript
Copy
Ask AI
// Express.js example
app.post('/webhooks/dakota', (req, res) => {
const event = req.body;
// Verify webhook signature (required for security)
const isValid = verifyWebhookSignature(
req.headers['x-webhook-signature'],
req.headers['x-webhook-timestamp'],
JSON.stringify(req.body),
process.env.DAKOTA_WEBHOOK_PUBLIC_KEY
);
if (!isValid) {
console.error('Webhook signature verification failed');
return res.status(401).send('Unauthorized');
}
// Process the event
handleWebhookEvent(event);
// Respond with 200 to acknowledge receipt
res.status(200).send('OK');
});
Python
Copy
Ask AI
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/dakota', methods=['POST'])
def handle_webhook():
event = request.get_json()
# Verify webhook signature (required for security)
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
is_valid = verify_webhook_signature(
signature,
timestamp,
request.get_data(as_text=True),
os.getenv('DAKOTA_WEBHOOK_PUBLIC_KEY')
)
if not is_valid:
print('Webhook signature verification failed')
return jsonify({'error': 'Unauthorized'}), 401
# Process the event
handle_webhook_event(event)
# Respond with 200 to acknowledge receipt
return jsonify({'status': 'ok'}), 200
Go
Copy
Ask AI
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
// Verify webhook signature (required for security)
isValid := verifyWebhookSignature(
signature,
timestamp,
string(body),
os.Getenv("DAKOTA_WEBHOOK_PUBLIC_KEY")
)
if !isValid {
log.Println("Webhook signature verification failed")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse webhook event
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
log.Printf("Failed to parse webhook event: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Process the event
handleWebhookEvent(event)
// Respond with 200 to acknowledge receipt
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhooks/dakota", webhookHandler)
log.Fatal(http.ListenAndServe(":3000", nil))
}
Rust
Copy
Ask AI
use axum::{extract::Request, response::Json, http::StatusCode};
use serde_json::Value;
use std::env;
async fn webhook_handler(req: Request) -> Result<StatusCode, (StatusCode, &'static str)> {
// Extract headers and body
let signature = req.headers()
.get("X-Webhook-Signature")
.and_then(|h| h.to_str().ok())
.ok_or((StatusCode::BAD_REQUEST, "Missing signature header"))?;
let timestamp = req.headers()
.get("X-Webhook-Timestamp")
.and_then(|h| h.to_str().ok())
.ok_or((StatusCode::BAD_REQUEST, "Missing timestamp header"))?;
let body = axum::body::to_bytes(req.into_body(), usize::MAX)
.await
.map_err(|_| (StatusCode::BAD_REQUEST, "Failed to read body"))?;
let body_str = String::from_utf8(body.to_vec())
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid UTF-8 in body"))?;
// Verify webhook signature (required for security)
let public_key = env::var("DAKOTA_WEBHOOK_PUBLIC_KEY")
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Missing public key"))?;
let is_valid = verify_webhook_signature(signature, timestamp, &body_str, &public_key)
.map_err(|_| (StatusCode::UNAUTHORIZED, "Signature verification failed"))?;
if !is_valid {
return Err((StatusCode::UNAUTHORIZED, "Invalid signature"));
}
// Parse webhook event
let event: Value = serde_json::from_str(&body_str)
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid JSON"))?;
// Process the event
handle_webhook_event(event)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to process event"))?;
Ok(StatusCode::OK)
}
Java
Copy
Ask AI
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
@RestController
public class WebhookController {
@PostMapping("/webhooks/dakota")
public ResponseEntity<String> handleWebhook(
@RequestBody Map<String, Object> event,
HttpServletRequest request) throws IOException {
String signature = request.getHeader("X-Webhook-Signature");
String timestamp = request.getHeader("X-Webhook-Timestamp");
// Get raw body for signature verification
String rawBody = objectMapper.writeValueAsString(event);
// Verify webhook signature (required for security)
String publicKey = System.getenv("DAKOTA_WEBHOOK_PUBLIC_KEY");
boolean isValid = verifyWebhookSignature(signature, timestamp, rawBody, publicKey);
if (!isValid) {
System.err.println("Webhook signature verification failed");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized");
}
// Process the event
handleWebhookEvent(event);
// Respond with 200 to acknowledge receipt
return ResponseEntity.ok("OK");
}
private void handleWebhookEvent(Map<String, Object> event) {
// Your event processing logic here
}
private boolean verifyWebhookSignature(String signature, String timestamp,
String body, String publicKey) {
// Signature verification implementation
return true; // Placeholder
}
}
2. Register Your Webhook
Register your endpoint with Dakota Platform:Webhook Target Fields
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | ✅ | HTTPS endpoint to receive webhooks | "https://your-app.com/webhooks/dakota" |
global | boolean | ❌ | Whether webhook receives events for all customers (default: false) | false |
event_types | array[string] | ❌ | Array of event types to subscribe to (defaults to all events if not specified) | ["transaction.status.updated", "customer.kyb_status.updated"] |
cURL
Copy
Ask AI
curl -X POST https://api.platform.dakota.xyz/webhooks/targets \
-H "X-API-Key: your-api-key" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/dakota",
"global": false,
"event_types": ["transaction.status.updated", "customer.kyb_status.updated"]
}'
JavaScript
Copy
Ask AI
const response = await fetch('https://api.platform.dakota.xyz/webhooks/targets', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://your-app.com/webhooks/dakota',
global: false,
event_types: ['transaction.status.updated', 'customer.kyb_status.updated']
})
});
const webhook = await response.json();
console.log('Created webhook target:', webhook.data.id);
Python
Copy
Ask AI
import requests
import uuid
response = requests.post(
'https://api.platform.dakota.xyz/webhooks/targets',
headers={
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={
'url': 'https://your-app.com/webhooks/dakota',
'global': False,
'event_types': ['transaction.status.updated', 'customer.kyb_status.updated']
}
)
webhook = response.json()
print(f'Created webhook target: {webhook["data"]["id"]}')
Go
Copy
Ask AI
package main
import (
"bytes"
"net/http"
"github.com/google/uuid"
)
func main() {
client := &http.Client{}
body := bytes.NewBufferString(`{
"url": "https://your-app.com/webhooks/dakota",
"global": false,
"event_types": ["transaction.status.updated", "customer.kyb_status.updated"]
}`)
req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/webhooks/targets", body)
req.Header.Add("X-API-Key", "your-api-key")
req.Header.Add("X-Idempotency-Key", uuid.New().String())
req.Header.Add("Content-Type", "application/json")
resp, _ := client.Do(req)
defer resp.Body.Close()
}
Rust
Copy
Ask AI
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
let body = json!({
"url": "https://your-app.com/webhooks/dakota",
"global": false,
"event_types": ["transaction.status.updated", "customer.kyb_status.updated"]
});
let response = client
.post("https://api.platform.dakota.xyz/webhooks/targets")
.headers(headers)
.json(&body)
.send()
.await?;
Ok(())
}
Java
Copy
Ask AI
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;
public class DakotaWebhookExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String body = """
{
"url": "https://your-app.com/webhooks/dakota",
"global": false,
"event_types": ["transaction.status.updated", "customer.kyb_status.updated"]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets"))
.header("X-API-Key", "your-api-key")
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
}
Copy
Ask AI
{
"data": {
"id": "wh_1234567890",
"url": "https://your-app.com/webhooks/dakota",
"global": false,
"event_types": ["transaction.status.updated", "customer.kyb_status.updated"],
"created_at": "2024-01-15T10:30:00Z"
}
}
Webhook Events
Transaction Events
transaction.status.updatedCopy
Ask AI
{
"event": "transaction.status.updated",
"data": {
"id": "31TgvvnYkN6edEJUTmF1LzTj2ug",
"status": "completed",
"previous_status": "processing",
"customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
"type": "onramp",
"amount": {
"value": "1000.00",
"currency": "USD"
}
},
"created_at": "2024-01-15T10:45:00Z",
"id": "evt_webhook_123"
}
Copy
Ask AI
{
"event": "transaction.failed",
"data": {
"id": "31TgvvnYkN6edEJUTmF1LzTj2ug",
"customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
"error": {
"code": "insufficient_funds",
"message": "Insufficient balance for transaction"
}
}
}
Customer Events
customer.kyb_status.updatedCopy
Ask AI
{
"event": "customer.kyb_status.updated",
"data": {
"id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
"kyb_status": "approved",
"previous_status": "pending"
}
}
Copy
Ask AI
{
"event": "customer.created",
"data": {
"id": "31TgvufZK3gDXBcA3BnSeLWiSn7"
}
}
Webhook Signature Verification
Dakota signs all webhooks with Ed25519 digital signatures for security:Node.js
Copy
Ask AI
const crypto = require('crypto');
const { Buffer } = require('buffer');
function verifyWebhookSignature(signature, timestamp, payload, dakotaPublicKeyHex) {
if (!signature || !timestamp || !payload || !dakotaPublicKeyHex) {
console.error('Missing required parameters for signature verification');
return false;
}
// Check timestamp to prevent replay attacks (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp);
const timeDifference = Math.abs(currentTime - webhookTime);
if (timeDifference > 300) { // 5 minutes
console.error('Webhook timestamp too old or too far in future');
return false;
}
// Create the signed payload (timestamp + body)
const signedPayload = timestamp + payload;
try {
// Decode the public key from hex and signature from base64
const publicKeyBuffer = Buffer.from(dakotaPublicKeyHex, 'hex');
const signatureBuffer = Buffer.from(signature, 'base64');
// Validate key length (Ed25519 public key should be 32 bytes)
if (publicKeyBuffer.length !== 32) {
console.error('Invalid Ed25519 public key length:', publicKeyBuffer.length);
return false;
}
// Wrap raw Ed25519 key in DER format for Node.js crypto API
const derPrefix = Buffer.from([
0x30, 0x2A, // SEQUENCE, 42 bytes
0x30, 0x05, // SEQUENCE, 5 bytes
0x06, 0x03, 0x2B, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
0x03, 0x21, 0x00 // BIT STRING, 33 bytes (including unused bits byte)
]);
const derKey = Buffer.concat([derPrefix, publicKeyBuffer]);
const publicKey = crypto.createPublicKey({
key: derKey,
format: 'der',
type: 'spki'
});
// Verify the signature using Ed25519
const isValid = crypto.verify(
null, // Ed25519 doesn't use a digest algorithm
Buffer.from(signedPayload, 'utf8'),
publicKey,
signatureBuffer
);
return isValid;
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
// Complete Express.js webhook handler example
const express = require('express');
const app = express();
// Middleware to capture raw body for signature verification
app.use('/webhooks/dakota', express.raw({ type: 'application/json' }));
app.post('/webhooks/dakota', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const rawBody = req.body.toString('utf8');
// Verify webhook signature
const isValid = verifyWebhookSignature(
signature,
timestamp,
rawBody,
process.env.DAKOTA_WEBHOOK_PUBLIC_KEY
);
if (!isValid) {
console.error('Webhook signature verification failed');
return res.status(401).send('Unauthorized');
}
try {
const event = JSON.parse(rawBody);
console.log('Received verified webhook:', event.event, event.id);
// Process the event
handleWebhookEvent(event);
// Respond with 200 to acknowledge receipt
res.status(200).send('OK');
} catch (error) {
console.error('Error processing webhook:', error);
res.status(400).send('Bad Request');
}
});
function handleWebhookEvent(event) {
// Make processing idempotent
if (isEventAlreadyProcessed(event.id)) {
console.log('Event already processed:', event.id);
return;
}
switch (event.event) {
case 'transaction.status.updated':
handleTransactionUpdate(event.data);
break;
case 'customer.kyb_status.updated':
handleCustomerUpdate(event.data);
break;
default:
console.log(`Unknown event type: ${event.event}`);
}
// Mark as processed
markEventAsProcessed(event.id);
}
function isEventAlreadyProcessed(eventId) {
// Check your database/cache for this event ID
// Return true if already processed
return false;
}
function markEventAsProcessed(eventId) {
// Store the event ID in your database/cache
console.log('Marking event as processed:', eventId);
}
function handleTransactionUpdate(data) {
console.log('Processing transaction update:', data.id, data.status);
// Your transaction handling logic here
}
function handleCustomerUpdate(data) {
console.log('Processing customer update:', data.id, data.kyb_status);
// Your customer handling logic here
}
Python
Copy
Ask AI
import base64
import json
import time
import os
from flask import Flask, request, jsonify
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
def verify_webhook_signature(signature, timestamp, body, dakota_public_key_hex):
if not all([signature, timestamp, body, dakota_public_key_hex]):
print('Missing required parameters for signature verification')
return False
try:
# Check timestamp to prevent replay attacks (within 5 minutes)
current_time = int(time.time())
webhook_time = int(timestamp)
time_difference = abs(current_time - webhook_time)
if time_difference > 300: # 5 minutes
print('Webhook timestamp too old or too far in future')
return False
# Create the signed payload (timestamp + body)
signed_payload = timestamp + body
# Decode the public key from hex and signature from base64
public_key_bytes = bytes.fromhex(dakota_public_key_hex)
signature_bytes = base64.b64decode(signature)
# Validate key length (Ed25519 public key should be 32 bytes)
if len(public_key_bytes) != 32:
print(f'Invalid Ed25519 public key length: {len(public_key_bytes)}')
return False
# Create Ed25519 public key object
public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
# Verify the signature
public_key.verify(signature_bytes, signed_payload.encode('utf-8'))
return True
except (InvalidSignature, ValueError, Exception) as e:
print(f'Signature verification failed: {e}')
return False
# Complete Flask webhook handler example
app = Flask(__name__)
processed_events = set() # Simple in-memory store (use database in production)
@app.route('/webhooks/dakota', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
raw_body = request.get_data(as_text=True)
# Verify webhook signature
is_valid = verify_webhook_signature(
signature,
timestamp,
raw_body,
os.getenv('DAKOTA_WEBHOOK_PUBLIC_KEY')
)
if not is_valid:
print('Webhook signature verification failed')
return jsonify({'error': 'Unauthorized'}), 401
try:
event = json.loads(raw_body)
print(f'Received verified webhook: {event.get("event")} {event.get("id")}')
# Process the event
handle_webhook_event(event)
# Respond with 200 to acknowledge receipt
return jsonify({'status': 'ok'}), 200
except (json.JSONDecodeError, KeyError) as e:
print(f'Error processing webhook: {e}')
return jsonify({'error': 'Bad Request'}), 400
def handle_webhook_event(event):
event_id = event.get('id')
# Make processing idempotent
if is_event_already_processed(event_id):
print(f'Event already processed: {event_id}')
return
event_type = event.get('event')
event_data = event.get('data', {})
if event_type == 'transaction.status.updated':
handle_transaction_update(event_data)
elif event_type == 'customer.kyb_status.updated':
handle_customer_update(event_data)
else:
print(f'Unknown event type: {event_type}')
# Mark as processed
mark_event_as_processed(event_id)
def is_event_already_processed(event_id):
# Check your database/cache for this event ID
return event_id in processed_events
def mark_event_as_processed(event_id):
# Store the event ID in your database/cache
processed_events.add(event_id)
print(f'Marking event as processed: {event_id}')
def handle_transaction_update(data):
transaction_id = data.get('id')
status = data.get('status')
print(f'Processing transaction update: {transaction_id} -> {status}')
# Your transaction handling logic here
def handle_customer_update(data):
customer_id = data.get('id')
kyb_status = data.get('kyb_status')
print(f'Processing customer update: {customer_id} -> {kyb_status}')
# Your customer handling logic here
if __name__ == '__main__':
app.run(port=3000, debug=True)
Rust
Copy
Ask AI
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use base64::{Engine as _, engine::general_purpose};
use std::time::{SystemTime, UNIX_EPOCH};
use warp::Filter;
use serde_json::{Value, json};
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::convert::Infallible;
#[derive(Debug)]
pub struct WebhookVerifier {
public_key: VerifyingKey,
processed_events: Arc<Mutex<HashSet<String>>>,
}
impl WebhookVerifier {
pub fn new(public_key_hex: &str) -> Result<Self, Box<dyn std::error::Error>> {
let public_key_bytes = hex::decode(public_key_hex)?;
// Validate key length (Ed25519 public key should be 32 bytes)
if public_key_bytes.len() != 32 {
return Err(format!("Invalid Ed25519 public key length: {}", public_key_bytes.len()).into());
}
let public_key = VerifyingKey::from_bytes(
&public_key_bytes.try_into().map_err(|_| "Invalid key length")?
)?;
Ok(Self {
public_key,
processed_events: Arc::new(Mutex::new(HashSet::new())),
})
}
pub fn verify_signature(&self, signature: &str, timestamp: &str, body: &str) -> Result<bool, Box<dyn std::error::Error>> {
if signature.is_empty() || timestamp.is_empty() || body.is_empty() {
println!("Missing required parameters for signature verification");
return Ok(false);
}
// Check timestamp to prevent replay attacks (within 5 minutes)
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs();
let webhook_time: u64 = timestamp.parse()?;
let time_difference = (current_time as i64 - webhook_time as i64).abs();
if time_difference > 300 {
println!("Webhook timestamp too old or too far in future");
return Ok(false);
}
// Create the signed payload (timestamp + body)
let signed_payload = format!("{}{}", timestamp, body);
// Decode signature
let signature_bytes = general_purpose::STANDARD.decode(signature)?;
let signature = Signature::from_bytes(&signature_bytes.try_into().map_err(|_| "Invalid signature length")?);
// Verify signature
match self.public_key.verify(signed_payload.as_bytes(), &signature) {
Ok(_) => Ok(true),
Err(e) => {
println!("Signature verification failed: {}", e);
Ok(false)
}
}
}
pub fn is_event_processed(&self, event_id: &str) -> bool {
self.processed_events.lock().unwrap().contains(event_id)
}
pub fn mark_event_processed(&self, event_id: &str) {
self.processed_events.lock().unwrap().insert(event_id.to_string());
println!("Marking event as processed: {}", event_id);
}
pub fn handle_webhook_event(&self, event: &Value) -> Result<(), Box<dyn std::error::Error>> {
let event_id = event["id"].as_str().unwrap_or("");
// Make processing idempotent
if self.is_event_processed(event_id) {
println!("Event already processed: {}", event_id);
return Ok(());
}
let event_type = event["event"].as_str().unwrap_or("");
let event_data = &event["data"];
match event_type {
"transaction.status.updated" => {
self.handle_transaction_update(event_data)?;
}
"customer.kyb_status.updated" => {
self.handle_customer_update(event_data)?;
}
_ => {
println!("Unknown event type: {}", event_type);
}
}
// Mark as processed
self.mark_event_processed(event_id);
Ok(())
}
fn handle_transaction_update(&self, data: &Value) -> Result<(), Box<dyn std::error::Error>> {
let transaction_id = data["id"].as_str().unwrap_or("");
let status = data["status"].as_str().unwrap_or("");
println!("Processing transaction update: {} -> {}", transaction_id, status);
// Your transaction handling logic here
Ok(())
}
fn handle_customer_update(&self, data: &Value) -> Result<(), Box<dyn std::error::Error>> {
let customer_id = data["id"].as_str().unwrap_or("");
let kyb_status = data["kyb_status"].as_str().unwrap_or("");
println!("Processing customer update: {} -> {}", customer_id, kyb_status);
// Your customer handling logic here
Ok(())
}
}
// Complete Warp webhook handler example
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let public_key_hex = std::env::var("DAKOTA_WEBHOOK_PUBLIC_KEY")
.expect("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required");
let verifier = Arc::new(WebhookVerifier::new(&public_key_hex)?);
let webhook_route = warp::path("webhooks")
.and(warp::path("dakota"))
.and(warp::post())
.and(warp::header::<String>("x-webhook-signature"))
.and(warp::header::<String>("x-webhook-timestamp"))
.and(warp::body::bytes())
.and_then({
let verifier = verifier.clone();
move |signature: String, timestamp: String, body: bytes::Bytes| {
let verifier = verifier.clone();
async move {
handle_webhook(verifier, signature, timestamp, body).await
}
}
});
let port = std::env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse::<u16>()
.unwrap_or(3000);
println!("Starting webhook server on port {}", port);
warp::serve(webhook_route)
.run(([0, 0, 0, 0], port))
.await;
Ok(())
}
async fn handle_webhook(
verifier: Arc<WebhookVerifier>,
signature: String,
timestamp: String,
body: bytes::Bytes,
) -> Result<impl warp::Reply, Infallible> {
let body_str = match String::from_utf8(body.to_vec()) {
Ok(s) => s,
Err(_) => {
return Ok(warp::reply::with_status(
"Invalid UTF-8 in body",
warp::http::StatusCode::BAD_REQUEST,
));
}
};
// Verify webhook signature
let is_valid = match verifier.verify_signature(&signature, ×tamp, &body_str) {
Ok(valid) => valid,
Err(e) => {
println!("Signature verification error: {}", e);
false
}
};
if !is_valid {
println!("Webhook signature verification failed");
return Ok(warp::reply::with_status(
"Unauthorized",
warp::http::StatusCode::UNAUTHORIZED,
));
}
// Parse webhook event
let event: Value = match serde_json::from_str(&body_str) {
Ok(e) => e,
Err(e) => {
println!("Failed to parse webhook event: {}", e);
return Ok(warp::reply::with_status(
"Bad Request",
warp::http::StatusCode::BAD_REQUEST,
));
}
};
println!("Received verified webhook: {} {}",
event["event"].as_str().unwrap_or("unknown"),
event["id"].as_str().unwrap_or("unknown")
);
// Process the event
if let Err(e) = verifier.handle_webhook_event(&event) {
println!("Error processing webhook event: {}", e);
return Ok(warp::reply::with_status(
"Internal Server Error",
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
));
}
Ok(warp::reply::with_status(
"OK",
warp::http::StatusCode::OK,
))
}
Java
Copy
Ask AI
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.time.Instant;
import java.nio.charset.StandardCharsets;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
public class WebhookVerifier {
private final PublicKey publicKey;
private final Set<String> processedEvents = ConcurrentHashMap.newKeySet();
public WebhookVerifier(String publicKeyHex) throws Exception {
byte[] publicKeyBytes = hexToBytes(publicKeyHex);
// Validate key length (Ed25519 public key should be 32 bytes)
if (publicKeyBytes.length != 32) {
throw new IllegalArgumentException("Invalid Ed25519 public key length: " + publicKeyBytes.length);
}
// Raw Ed25519 key - wrap in DER format for Java crypto API
byte[] derKey = wrapRawKeyInDER(publicKeyBytes);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey);
KeyFactory keyFactory = KeyFactory.getInstance("EdDSA");
this.publicKey = keyFactory.generatePublic(keySpec);
}
private byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
private byte[] wrapRawKeyInDER(byte[] rawKey) {
// Ed25519 DER prefix for public key
byte[] derPrefix = {
0x30, 0x2A, // SEQUENCE, 42 bytes
0x30, 0x05, // SEQUENCE, 5 bytes
0x06, 0x03, 0x2B, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
0x03, 0x21, 0x00 // BIT STRING, 33 bytes (including unused bits byte)
};
byte[] derKey = new byte[derPrefix.length + rawKey.length];
System.arraycopy(derPrefix, 0, derKey, 0, derPrefix.length);
System.arraycopy(rawKey, 0, derKey, derPrefix.length, rawKey.length);
return derKey;
}
public boolean verifySignature(String signature, String timestamp, String body) {
if (signature == null || timestamp == null || body == null ||
signature.isEmpty() || timestamp.isEmpty() || body.isEmpty()) {
System.err.println("Missing required parameters for signature verification");
return false;
}
try {
// Check timestamp to prevent replay attacks (within 5 minutes)
long currentTime = Instant.now().getEpochSecond();
long webhookTime = Long.parseLong(timestamp);
long timeDifference = Math.abs(currentTime - webhookTime);
if (timeDifference > 300) { // 5 minutes
System.err.println("Webhook timestamp too old or too far in future");
return false;
}
// Create the signed payload (timestamp + body)
String signedPayload = timestamp + body;
// Decode signature
byte[] signatureBytes = Base64.getDecoder().decode(signature);
// Verify signature using Ed25519
Signature verifier = Signature.getInstance("EdDSA");
verifier.initVerify(publicKey);
verifier.update(signedPayload.getBytes(StandardCharsets.UTF_8));
return verifier.verify(signatureBytes);
} catch (Exception e) {
System.err.println("Signature verification failed: " + e.getMessage());
return false;
}
}
public boolean isEventProcessed(String eventId) {
return processedEvents.contains(eventId);
}
public void markEventProcessed(String eventId) {
processedEvents.add(eventId);
System.out.println("Marking event as processed: " + eventId);
}
public void handleWebhookEvent(Map<String, Object> event) throws Exception {
String eventId = (String) event.get("id");
// Make processing idempotent
if (isEventProcessed(eventId)) {
System.out.println("Event already processed: " + eventId);
return;
}
String eventType = (String) event.get("event");
@SuppressWarnings("unchecked")
Map<String, Object> eventData = (Map<String, Object>) event.get("data");
switch (eventType) {
case "transaction.status.updated":
handleTransactionUpdate(eventData);
break;
case "customer.kyb_status.updated":
handleCustomerUpdate(eventData);
break;
default:
System.out.println("Unknown event type: " + eventType);
}
// Mark as processed
markEventProcessed(eventId);
}
private void handleTransactionUpdate(Map<String, Object> data) {
String transactionId = (String) data.get("id");
String status = (String) data.get("status");
System.out.println("Processing transaction update: " + transactionId + " -> " + status);
// Your transaction handling logic here
}
private void handleCustomerUpdate(Map<String, Object> data) {
String customerId = (String) data.get("id");
String kybStatus = (String) data.get("kyb_status");
System.out.println("Processing customer update: " + customerId + " -> " + kybStatus);
// Your customer handling logic here
}
}
@RestController
public class WebhookController {
private final WebhookVerifier webhookVerifier;
public WebhookController() throws Exception {
String publicKeyHex = System.getenv("DAKOTA_WEBHOOK_PUBLIC_KEY");
if (publicKeyHex == null || publicKeyHex.isEmpty()) {
throw new IllegalStateException("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required");
}
this.webhookVerifier = new WebhookVerifier(publicKeyHex);
}
@PostMapping("/webhooks/dakota")
public ResponseEntity<String> handleWebhook(
@RequestBody Map<String, Object> event,
HttpServletRequest request) throws IOException {
String signature = request.getHeader("X-Webhook-Signature");
String timestamp = request.getHeader("X-Webhook-Timestamp");
// Get raw body for signature verification
String rawBody = new com.fasterxml.jackson.databind.ObjectMapper()
.writeValueAsString(event);
// Verify webhook signature
boolean isValid = webhookVerifier.verifySignature(signature, timestamp, rawBody);
if (!isValid) {
System.err.println("Webhook signature verification failed");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized");
}
System.out.println("Received verified webhook: " +
event.get("event") + " " + event.get("id"));
try {
// Process the event
webhookVerifier.handleWebhookEvent(event);
// Respond with 200 to acknowledge receipt
return ResponseEntity.ok("OK");
} catch (Exception e) {
System.err.println("Error processing webhook event: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Internal Server Error");
}
}
}
Go
Copy
Ask AI
package main
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
)
type WebhookEvent struct {
ID string `json:"id"`
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
CreatedAt string `json:"created_at"`
}
type WebhookHandler struct {
publicKey ed25519.PublicKey
processedEvents map[string]bool
mutex sync.RWMutex
}
func NewWebhookHandler(publicKeyHex string) (*WebhookHandler, error) {
publicKeyBytes, err := hex.DecodeString(publicKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to decode public key: %v", err)
}
// Validate key length (Ed25519 public key should be 32 bytes)
if len(publicKeyBytes) != 32 {
return nil, fmt.Errorf("invalid Ed25519 public key length: %d", len(publicKeyBytes))
}
return &WebhookHandler{
publicKey: ed25519.PublicKey(publicKeyBytes),
processedEvents: make(map[string]bool),
}, nil
}
func (wh *WebhookHandler) verifySignature(signature, timestamp, body string) bool {
if signature == "" || timestamp == "" || body == "" {
log.Println("Missing required parameters for signature verification")
return false
}
// Check timestamp to prevent replay attacks (within 5 minutes)
webhookTime, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
log.Printf("Invalid timestamp format: %v", err)
return false
}
currentTime := time.Now().Unix()
timeDifference := currentTime - webhookTime
if timeDifference > 300 || timeDifference < -300 { // 5 minutes
log.Println("Webhook timestamp too old or too far in future")
return false
}
// Create the signed payload
signedPayload := timestamp + body
// Decode signature
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
log.Printf("Failed to decode signature: %v", err)
return false
}
// Verify signature
return ed25519.Verify(wh.publicKey, []byte(signedPayload), signatureBytes)
}
func (wh *WebhookHandler) isEventProcessed(eventID string) bool {
wh.mutex.RLock()
defer wh.mutex.RUnlock()
return wh.processedEvents[eventID]
}
func (wh *WebhookHandler) markEventProcessed(eventID string) {
wh.mutex.Lock()
defer wh.mutex.Unlock()
wh.processedEvents[eventID] = true
log.Printf("Marking event as processed: %s", eventID)
}
func (wh *WebhookHandler) handleWebhookEvent(event WebhookEvent) {
// Make processing idempotent
if wh.isEventProcessed(event.ID) {
log.Printf("Event already processed: %s", event.ID)
return
}
switch event.Event {
case "transaction.status.updated":
wh.handleTransactionUpdate(event.Data)
case "customer.kyb_status.updated":
wh.handleCustomerUpdate(event.Data)
default:
log.Printf("Unknown event type: %s", event.Event)
}
// Mark as processed
wh.markEventProcessed(event.ID)
}
func (wh *WebhookHandler) handleTransactionUpdate(data map[string]interface{}) {
transactionID, _ := data["id"].(string)
status, _ := data["status"].(string)
log.Printf("Processing transaction update: %s -> %s", transactionID, status)
// Your transaction handling logic here
}
func (wh *WebhookHandler) handleCustomerUpdate(data map[string]interface{}) {
customerID, _ := data["id"].(string)
kybStatus, _ := data["kyb_status"].(string)
log.Printf("Processing customer update: %s -> %s", customerID, kybStatus)
// Your customer handling logic here
}
func (wh *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
// Verify webhook signature
if !wh.verifySignature(signature, timestamp, string(body)) {
log.Println("Webhook signature verification failed")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse webhook event
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
log.Printf("Failed to parse webhook event: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
log.Printf("Received verified webhook: %s %s", event.Event, event.ID)
// Process the event
wh.handleWebhookEvent(event)
// Respond with 200 to acknowledge receipt
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
publicKeyHex := os.Getenv("DAKOTA_WEBHOOK_PUBLIC_KEY")
if publicKeyHex == "" {
log.Fatal("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required")
}
webhookHandler, err := NewWebhookHandler(publicKeyHex)
if err != nil {
log.Fatalf("Failed to create webhook handler: %v", err)
}
http.Handle("/webhooks/dakota", webhookHandler)
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Printf("Starting webhook server on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Signature Headers
Dakota includes these headers in webhook requests:X-Webhook-Signature: Ed25519 signature for verification (base64 encoded)X-Webhook-Timestamp: Unix timestamp when webhook was sentX-Dakota-Event-ID: Unique identifier for this webhook event (optional)
| Environment | Public Key (hex-encoded) |
|---|---|
| Production | 65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4 |
| Sandbox | 7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f |
Managing Webhooks
List Webhooks
List all webhook targets configured for your account:cURL
Copy
Ask AI
curl -X GET https://api.platform.dakota.xyz/webhooks/targets \
-H "X-API-Key: your-api-key"
JavaScript
Copy
Ask AI
const response = await fetch('https://api.platform.dakota.xyz/webhooks/targets', {
headers: {
'X-API-Key': 'your-api-key'
}
});
const webhooks = await response.json();
console.log(`Found ${webhooks.data.length} webhook targets`);
Python
Copy
Ask AI
import requests
response = requests.get(
'https://api.platform.dakota.xyz/webhooks/targets',
headers={'X-API-Key': 'your-api-key'}
)
webhooks = response.json()
print(f'Found {len(webhooks["data"])} webhook targets')
Go
Copy
Ask AI
package main
import (
"net/http"
)
func main() {
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/webhooks/targets", nil)
req.Header.Add("X-API-Key", "your-api-key")
resp, _ := client.Do(req)
defer resp.Body.Close()
}
Rust
Copy
Ask AI
use reqwest::header::{HeaderMap, HeaderValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
let response = client
.get("https://api.platform.dakota.xyz/webhooks/targets")
.headers(headers)
.send()
.await?;
Ok(())
}
Java
Copy
Ask AI
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class DakotaListWebhooksExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets"))
.header("X-API-Key", "your-api-key")
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
}
Update Webhook
Update an existing webhook target:cURL
Copy
Ask AI
curl -X PATCH https://api.platform.dakota.xyz/webhooks/targets/{webhook_id} \
-H "X-API-Key: your-api-key" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"global": false
}'
JavaScript
Copy
Ask AI
const response = await fetch(`https://api.platform.dakota.xyz/webhooks/targets/${webhookId}`, {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
global: false
})
});
Python
Copy
Ask AI
import requests
import uuid
response = requests.patch(
f'https://api.platform.dakota.xyz/webhooks/targets/{webhook_id}',
headers={
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={'global': False}
)
Go
Copy
Ask AI
package main
import (
"bytes"
"net/http"
"github.com/google/uuid"
)
func main() {
client := &http.Client{}
body := bytes.NewBufferString(`{"global": false}`)
req, _ := http.NewRequest("PATCH", "https://api.platform.dakota.xyz/webhooks/targets/" + webhookId, body)
req.Header.Add("X-API-Key", "your-api-key")
req.Header.Add("X-Idempotency-Key", uuid.New().String())
req.Header.Add("Content-Type", "application/json")
resp, _ := client.Do(req)
defer resp.Body.Close()
}
Rust
Copy
Ask AI
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
let body = json!({"global": false});
let response = client
.patch(&format!("https://api.platform.dakota.xyz/webhooks/targets/{}", webhook_id))
.headers(headers)
.json(&body)
.send()
.await?;
Ok(())
}
Java
Copy
Ask AI
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;
public class DakotaUpdateWebhookExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String body = "{\"global\": false}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets/" + webhookId))
.header("X-API-Key", "your-api-key")
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
}
Delete Webhook
Delete a webhook target:cURL
Copy
Ask AI
curl -X DELETE https://api.platform.dakota.xyz/webhooks/targets/{webhook_id} \
-H "X-API-Key: your-api-key" \
-H "X-Idempotency-Key: $(uuidgen)"
JavaScript
Copy
Ask AI
const response = await fetch(`https://api.platform.dakota.xyz/webhooks/targets/${webhookId}`, {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': crypto.randomUUID()
}
});
if (response.ok) {
console.log('Webhook deleted successfully');
}
Python
Copy
Ask AI
import requests
import uuid
response = requests.delete(
f'https://api.platform.dakota.xyz/webhooks/targets/{webhook_id}',
headers={
'X-API-Key': 'your-api-key',
'X-Idempotency-Key': str(uuid.uuid4())
}
)
if response.status_code == 204:
print('Webhook deleted successfully')
Go
Copy
Ask AI
package main
import (
"net/http"
"github.com/google/uuid"
)
func main() {
client := &http.Client{}
req, _ := http.NewRequest("DELETE", "https://api.platform.dakota.xyz/webhooks/targets/" + webhookId, nil)
req.Header.Add("X-API-Key", "your-api-key")
req.Header.Add("X-Idempotency-Key", uuid.New().String())
resp, _ := client.Do(req)
defer resp.Body.Close()
}
Rust
Copy
Ask AI
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
let response = client
.delete(&format!("https://api.platform.dakota.xyz/webhooks/targets/{}", webhook_id))
.headers(headers)
.send()
.await?;
Ok(())
}
Java
Copy
Ask AI
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;
public class DakotaDeleteWebhookExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets/" + webhookId))
.header("X-API-Key", "your-api-key")
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.DELETE()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
}
Webhook Delivery
Retry Policy
Dakota automatically retries failed webhook deliveries using exponential backoff over approximately 48 hours:| Attempt | Delay After Failure |
|---|---|
| 1 | 5 minutes |
| 2 | 10 minutes |
| 3 | 15 minutes |
| 4 | 30 minutes |
| 5 | 1 hour |
| 6 | 2 hours |
| 7 | 4 hours |
| 8 | 8 hours |
| 9 | 12 hours |
| 10 (final) | 20 hours |
- Max attempts: 10 total
- Total retry window: ~48 hours
- Timeout per attempt: 20 seconds
Success Criteria
A webhook delivery is considered successful when:- Your endpoint returns an HTTP status code in the
2xxrange (200-299) - Response is received within 20 seconds
- Your endpoint returns a non-2xx status code (4xx, 5xx)
- Connection timeout (20 seconds exceeded)
- Connection refused or DNS failure
Failure Handling
If all 10 delivery attempts fail:- Webhook is marked as permanently failed
- You can view failed webhooks in the dashboard
- Failed webhooks can be manually retried
Webhook Headers
Every webhook request includes these headers:| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Dakota-Webhooks/1.0 |
X-Dakota-Event-ID | Unique event identifier (use for idempotency) |
X-Dakota-Event-Type | The type of event (e.g., transaction.completed) |
X-Dakota-Delivery-Attempt | Current attempt number (1-10) |
X-Webhook-Signature | Ed25519 signature (base64 encoded) |
X-Webhook-Timestamp | Unix timestamp when webhook was sent |
X-Dakota-Event-ID for idempotency - it remains the same across all retry attempts for the same event.
Best Practices
Endpoint Requirements
- HTTPS only: Dakota Platform only sends webhooks to HTTPS endpoints
- Fast response: Respond within 30 seconds
- 2xx status codes: Return 200-299 status for successful processing
- Idempotent: Handle duplicate webhooks gracefully
Security
- Always verify webhook signatures
- Use HTTPS for your webhook endpoint
- Validate event data before processing
- Log webhook events for debugging
Processing Best Practices
Implement robust webhook event processing with error handling, retries, and idempotency:Node.js
Copy
Ask AI
// Robust webhook event processing with error handling
class WebhookProcessor {
constructor() {
this.processedEvents = new Set(); // Use Redis/database in production
this.maxRetries = 3;
}
async handleWebhookEvent(event) {
const eventId = event.id;
// Make processing idempotent
if (this.isEventAlreadyProcessed(eventId)) {
console.log('Event already processed:', eventId);
return { success: true, message: 'Already processed' };
}
try {
await this.processEventWithRetry(event);
this.markEventAsProcessed(eventId);
return { success: true, message: 'Event processed successfully' };
} catch (error) {
console.error('Failed to process event after retries:', error);
await this.storeFailedEvent(event, error);
throw error;
}
}
async processEventWithRetry(event, attempt = 1) {
try {
await this.processEvent(event);
} catch (error) {
if (attempt < this.maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(`Retrying event processing in ${delay}ms (attempt ${attempt + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.processEventWithRetry(event, attempt + 1);
}
throw error;
}
}
async processEvent(event) {
console.log(`Processing event: ${event.event} (${event.id})`);
switch (event.event) {
case 'transaction.status.updated':
await this.handleTransactionUpdate(event.data);
break;
case 'transaction.completed':
await this.handleTransactionCompleted(event.data);
break;
case 'transaction.failed':
await this.handleTransactionFailed(event.data);
break;
case 'customer.kyb_status.updated':
await this.handleCustomerKybUpdate(event.data);
break;
case 'customer.created':
await this.handleCustomerCreated(event.data);
break;
default:
console.log(`Unknown event type: ${event.event}`);
}
}
async handleTransactionUpdate(data) {
console.log(`Transaction ${data.id} status: ${data.previous_status} → ${data.status}`);
await this.updateTransactionStatus(data.id, data.status);
if (data.status === 'completed') {
await this.notifyTransactionCompleted(data);
} else if (data.status === 'failed') {
await this.notifyTransactionFailed(data);
}
}
async handleTransactionCompleted(data) {
console.log(`Transaction completed: ${data.id}`);
await this.updateTransactionStatus(data.id, 'completed');
await this.notifyTransactionCompleted(data);
await this.triggerPostCompletionActions(data);
}
async handleTransactionFailed(data) {
console.log(`Transaction failed: ${data.id}`, data.error);
await this.updateTransactionStatus(data.id, 'failed');
await this.notifyTransactionFailed(data);
await this.handleTransactionFailure(data);
}
async handleCustomerKybUpdate(data) {
console.log(`Customer ${data.id} KYB status: ${data.previous_status} → ${data.kyb_status}`);
await this.updateCustomerKybStatus(data.id, data.kyb_status);
if (data.kyb_status === 'approved') {
await this.notifyCustomerApproved(data);
await this.enableCustomerFeatures(data.id);
} else if (data.kyb_status === 'rejected') {
await this.notifyCustomerRejected(data);
await this.handleKybRejection(data);
}
}
async handleCustomerCreated(data) {
console.log(`New customer created: ${data.id}`);
await this.syncCustomerToDatabase(data);
await this.sendWelcomeEmail(data);
}
isEventAlreadyProcessed(eventId) {
return this.processedEvents.has(eventId);
}
markEventAsProcessed(eventId) {
this.processedEvents.add(eventId);
console.log('Marked event as processed:', eventId);
}
async storeFailedEvent(event, error) {
console.log('Storing failed event for review:', event.id, error.message);
// Store in database for manual review
}
// Business logic methods (implement according to your needs)
async updateTransactionStatus(transactionId, status) { /* Your implementation */ }
async notifyTransactionCompleted(data) { /* Your implementation */ }
async notifyTransactionFailed(data) { /* Your implementation */ }
async triggerPostCompletionActions(data) { /* Your implementation */ }
async handleTransactionFailure(data) { /* Your implementation */ }
async updateCustomerKybStatus(customerId, status) { /* Your implementation */ }
async notifyCustomerApproved(data) { /* Your implementation */ }
async notifyCustomerRejected(data) { /* Your implementation */ }
async enableCustomerFeatures(customerId) { /* Your implementation */ }
async handleKybRejection(data) { /* Your implementation */ }
async syncCustomerToDatabase(data) { /* Your implementation */ }
async sendWelcomeEmail(data) { /* Your implementation */ }
}
// Usage in Express route
const webhookProcessor = new WebhookProcessor();
app.post('/webhooks/dakota', async (req, res) => {
// ... signature verification code ...
try {
const result = await webhookProcessor.handleWebhookEvent(event);
console.log(result.message);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(200).send('Processing failed but acknowledged');
}
});
Python
Copy
Ask AI
# Python implementation here - abbreviated for clarity
Go
Copy
Ask AI
# Go implementation here - abbreviated for clarity
Rust
Copy
Ask AI
# Rust implementation here - abbreviated for clarity
Java
Copy
Ask AI
# Java implementation here - abbreviated for clarity
Testing Webhooks
Local Development
Use tools like ngrok to expose local endpoints:Copy
Ask AI
ngrok http 3000
# Use the HTTPS URL for webhook registration
Webhook Testing
Test your webhook endpoint manually:Copy
Ask AI
curl -X POST https://your-app.com/webhooks/dakota \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: base64_encoded_signature" \
-H "X-Webhook-Timestamp: $(date +%s)" \
-d '{
"event": "transaction.status.updated",
"data": {
"id": "31TgvtxUdXi95dUN4M8X1rhSCNS",
"status": "completed"
}
}'
Troubleshooting
Common Issues
Webhooks Not Received- Check that your endpoint returns 2xx status
- Verify your URL is accessible from the internet
- Ensure HTTPS is properly configured
- Check firewall/proxy settings
- Ensure you’re using Dakota Platform’s correct Ed25519 public key
- Verify the signature calculation matches our Ed25519 implementation
- Check that the timestamp and payload haven’t been modified
- Ensure you’re using the correct header names (
X-Webhook-Signature,X-Webhook-Timestamp)
- Implement idempotency using the event ID
- Store processed event IDs to prevent duplicates
- Use database constraints where possible
Monitoring
- Set up alerts for webhook failures
- Log all webhook events for debugging
- Monitor endpoint response times
- Track webhook delivery success rates
Event Types Reference
Based on the platform’s actual event definitions, here are the available webhook event types:| Event | Description |
|---|---|
| Customer Events | |
customer.created | New customer created |
customer.updated | Customer information updated |
customer.kyb_status.created | Customer KYB status created |
customer.kyb_status.updated | Customer KYB status changed |
customer.kyb_link.created | Customer KYB link created |
customer.kyb_link.updated | Customer KYB link updated |
| Transaction Events | |
transaction.auto.created | Auto account transaction created |
transaction.auto.updated | Auto account transaction updated |
transaction.one_off.created | One-off transaction created |
transaction.one_off.updated | One-off transaction updated |
| Account Events | |
auto_account.created | Auto account created |
auto_account.updated | Auto account updated |
auto_account.deleted | Auto account deleted |
| Other Events | |
user.created | User created |
user.updated | User updated |
user.deleted | User deleted |
api_key.created | API key created |
api_key.deleted | API key deleted |
recipient.created | Recipient created |
recipient.updated | Recipient updated |
recipient.deleted | Recipient deleted |
destination.created | Destination created |
destination.deleted | Destination deleted |
target.created | Target created |
target.updated | Target updated |
target.deleted | Target deleted |
exception.created | Exception created |
exception.cleared | Exception cleared |
transaction.status.updated for clarity, but the actual platform uses the specific event types listed above.
Next Steps
After setting up webhooks:- Testing Your Integration - Test webhook delivery and processing
- Transactions - Process transactions that trigger webhook events
- Customer Onboarding - Set up KYB status webhooks
API Reference
For detailed endpoint documentation, see:- Webhooks API Reference - Webhook target management
- Events API Reference - Event types and payload formats
- Webhook Security Guide - Security best practices