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": "approved",
    "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: Use the appropriate Ed25519 public key for your environment:
EnvironmentPublic Key (hex-encoded)
Production65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4
Sandbox7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f
The public key is a hex-encoded string (64 characters representing 32 bytes).

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 === 'approved') {
      await this.notifyCustomerApproved(data);
      await this.enableCustomerFeatures(data.id);
    } else if (data.kyb_status === 'rejected') {
      await this.notifyCustomerRejected(data);
      await this.handleKybRejection(data);
    }
  }

  async handleCustomerCreated(data) {
    console.log(`New customer created: ${data.id}`);
    await this.syncCustomerToDatabase(data);
    await this.sendWelcomeEmail(data);
  }

  isEventAlreadyProcessed(eventId) {
    return this.processedEvents.has(eventId);
  }

  markEventAsProcessed(eventId) {
    this.processedEvents.add(eventId);
    console.log('Marked event as processed:', eventId);
  }

  async storeFailedEvent(event, error) {
    console.log('Storing failed event for review:', event.id, error.message);
    // Store in database for manual review
  }

  // Business logic methods (implement according to your needs)
  async updateTransactionStatus(transactionId, status) { /* Your implementation */ }
  async notifyTransactionCompleted(data) { /* Your implementation */ }
  async notifyTransactionFailed(data) { /* Your implementation */ }
  async triggerPostCompletionActions(data) { /* Your implementation */ }
  async handleTransactionFailure(data) { /* Your implementation */ }
  async updateCustomerKybStatus(customerId, status) { /* Your implementation */ }
  async notifyCustomerApproved(data) { /* Your implementation */ }
  async notifyCustomerRejected(data) { /* Your implementation */ }
  async enableCustomerFeatures(customerId) { /* Your implementation */ }
  async handleKybRejection(data) { /* Your implementation */ }
  async syncCustomerToDatabase(data) { /* Your implementation */ }
  async sendWelcomeEmail(data) { /* Your implementation */ }
}

// Usage in Express route
const webhookProcessor = new WebhookProcessor();

app.post('/webhooks/dakota', async (req, res) => {
  // ... signature verification code ...
  
  try {
    const result = await webhookProcessor.handleWebhookEvent(event);
    console.log(result.message);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing failed:', error);
    res.status(200).send('Processing failed but acknowledged');
  }
});
Python
# 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: