Skip to main content
Webhooks allow Dakota Platform to send real-time notifications about events in your account directly to your application.

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
// 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
# 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
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
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
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

FieldTypeRequiredDescriptionExample
urlstringHTTPS endpoint to receive webhooks"https://your-app.com/webhooks/dakota"
globalbooleanWhether webhook receives events for all customers (default: false)false
event_typesarray[string]Array of event types to subscribe to (defaults to all events if not specified)["transaction.status.updated", "customer.kyb_status.updated"]
cURL
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
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
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
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
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
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());
    }
}
Response:
{
  "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.updated
{
  "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"
}
transaction.failed
{
  "event": "transaction.failed",
  "data": {
    "id": "31TgvvnYkN6edEJUTmF1LzTj2ug",
    "customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
    "error": {
      "code": "insufficient_funds",
      "message": "Insufficient balance for transaction"
    }
  }
}

Customer Events

customer.kyb_status.updated
{
  "event": "customer.kyb_status.updated",
  "data": {
    "id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
    "kyb_status": "active",
    "previous_status": "pending"
  }
}
customer.created
{
  "event": "customer.created",
  "data": {
    "id": "31TgvufZK3gDXBcA3BnSeLWiSn7"
  }
}

Webhook Signature Verification

Dakota signs all webhooks with Ed25519 digital signatures for security:
Node.js
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
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
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, &timestamp, &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
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
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 sent
  • X-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.
EnvironmentPublic Key (hex-encoded)
Production65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4
Sandbox7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f
The public key is a hex-encoded string (64 characters representing 32 bytes). Set this as DAKOTA_WEBHOOK_PUBLIC_KEY in your environment.

Managing Webhooks

List Webhooks

List all webhook targets configured for your account:
cURL
curl -X GET https://api.platform.dakota.xyz/webhooks/targets \
  -H "X-API-Key: your-api-key"
JavaScript
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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:
AttemptDelay After Failure
15 minutes
210 minutes
315 minutes
430 minutes
51 hour
62 hours
74 hours
88 hours
912 hours
10 (final)20 hours
Retry behavior:
  • 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 2xx range (200-299)
  • Response is received within 20 seconds
A webhook delivery fails and triggers a retry when:
  • 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:
HeaderDescription
Content-Typeapplication/json
User-AgentDakota-Webhooks/1.0
X-Dakota-Event-IDUnique event identifier (use for idempotency)
X-Dakota-Event-TypeThe type of event (e.g., transaction.completed)
X-Dakota-Delivery-AttemptCurrent attempt number (1-10)
X-Webhook-SignatureEd25519 signature (base64 encoded)
X-Webhook-TimestampUnix timestamp when webhook was sent
Use 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
// 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
# Python implementation here - abbreviated for clarity
Go
# Go implementation here - abbreviated for clarity  
Rust
# Rust implementation here - abbreviated for clarity
Java
# Java implementation here - abbreviated for clarity

Testing Webhooks

Local Development

Use tools like ngrok to expose local endpoints:
ngrok http 3000
# Use the HTTPS URL for webhook registration

Webhook Testing

Test your webhook endpoint manually:
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
Signature Verification Failing
  • 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)
Duplicate Webhooks
  • 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:
EventDescription
Customer Events
customer.createdNew customer created
customer.updatedCustomer information updated
customer.kyb_status.createdCustomer KYB status created
customer.kyb_status.updatedCustomer KYB status changed
customer.kyb_link.createdCustomer KYB link created
customer.kyb_link.updatedCustomer KYB link updated
Transaction Events
transaction.auto.createdAuto account transaction created
transaction.auto.updatedAuto account transaction updated
transaction.one_off.createdOne-off transaction created
transaction.one_off.updatedOne-off transaction updated
Account Events
auto_account.createdAuto account created
auto_account.updatedAuto account updated
auto_account.deletedAuto account deleted
Other Events
user.createdUser created
user.updatedUser updated
user.deletedUser deleted
api_key.createdAPI key created
api_key.deletedAPI key deleted
recipient.createdRecipient created
recipient.updatedRecipient updated
recipient.deletedRecipient deleted
destination.createdDestination created
destination.deletedDestination deleted
target.createdTarget created
target.updatedTarget updated
target.deletedTarget deleted
exception.createdException created
exception.clearedException cleared
Note: The examples in this documentation use simplified event names like transaction.status.updated for clarity, but the actual platform uses the specific event types listed above.

Next Steps

After setting up webhooks:
  1. Testing Your Integration - Test webhook delivery and processing
  2. Transactions - Process transactions that trigger webhook events
  3. Customer Onboarding - Set up KYB status webhooks

API Reference

For detailed endpoint documentation, see: