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": "active",
"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)
Dakota Platform’s Public Keys for Webhook VerificationDakota uses Ed25519 digital signatures (not HMAC shared secrets). You need Dakota’s public key to verify webhook signatures.
| Environment | Public Key (hex-encoded) |
|---|---|
| Production | 65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4 |
| Sandbox | 7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f |
DAKOTA_WEBHOOK_PUBLIC_KEY in your environment.
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 === 'active') {
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