Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt

Use this file to discover all available pages before exploring further.

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

Webhook Signature Verification

Dakota signs every webhook with an Ed25519 signature (not HMAC). To verify a delivery, you need Dakota’s public key — set it in your environment as DAKOTA_WEBHOOK_PUBLIC_KEY.

Dakota Public Keys

EnvironmentPublic Key (hex-encoded)
Production65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4
Sandbox7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f
Each key is 64 hex characters (32 raw bytes).

Signature Headers

Dakota includes these headers in every webhook request:
HeaderDescription
X-Webhook-SignatureBase64-encoded Ed25519 signature over timestamp + body
X-Webhook-TimestampUnix timestamp when the webhook was sent — reject anything older than 5 minutes to prevent replay attacks
X-Dakota-Event-IDUnique identifier for the event (optional, useful for idempotency)
Full verification implementations for Node.js, Python, Go, Rust, and Java are in Verification Code Examples below.

Setting Up Webhooks

1. Create a Webhook Endpoint

Create an endpoint in your application to receive webhook notifications:
// 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');
});

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.auto.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.auto.updated", "customer.kyb_status.updated"]
  }'
Response:
{
  "data": {
    "id": "wh_1234567890",
    "url": "https://your-app.com/webhooks/dakota",
    "global": false,
    "event_types": ["transaction.auto.updated", "customer.kyb_status.updated"],
    "created_at": "2024-01-15T10:30:00Z"
  }
}
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.auto.updated', 'customer.kyb_status.updated']
  })
});

const webhook = await response.json();
console.log('Created webhook target:', webhook.data.id);

Webhook Events

Transaction Events

Transaction lifecycle events fire for auto-account (onramp/offramp/swap) and one-off transactions. There is no separate failed event — failure is reflected in the status field of the *.updated event. transaction.auto.created — emitted when Dakota creates a transaction for an onramp/offramp/swap account (e.g., after detecting a bank deposit).
{
  "event": "transaction.auto.created",
  "data": {
    "id": "31TgvvnYkN6edEJUTmF1LzTj2ug",
    "status": "pending",
    "customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
    "type": "onramp",
    "amount": {
      "value": "1000.00",
      "currency": "USD"
    }
  },
  "created_at": "2024-01-15T10:45:00Z",
  "id": "evt_webhook_123"
}
transaction.auto.updated — emitted on every status change or detail update (inspect data.status to distinguish processing, completed, failed).
{
  "event": "transaction.auto.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.one_off.created / transaction.one_off.updated — same shape, emitted for single-use transactions created via POST /transactions/one-off. Covers both off-ramp (crypto → fiat with payment_reference support) and swap (crypto → crypto with a destination network override). The one-off artifact is discarded after the transaction completes.

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"
  }
}
customer.kyb_application.submitted
{
  "event": "customer.kyb_application.submitted",
  "data": {
    "customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7",
    "type": "business"
  }
}

Verification Code Examples

Ed25519 verification implementations — pick your language. See the public keys and required headers in Webhook Signature Verification above.
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.auto.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
}

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"
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`);

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
  }'
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
  })
});

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)"
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');
}

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

Event Ordering

Webhooks are not guaranteed to arrive in lifecycle order. Two events fired close together for the same resource (e.g. wallet.transaction.updated for Broadcasted and Success on a fast testnet, or transaction.auto.deposit.received and transaction.auto.deposit.succeeded for an instant rail) may reach your endpoint out of order due to network races, retry timing, and the fact that the envelope created field is second-resolution. This is most visible in sandbox because testnet broadcasts and the simulate endpoints transition states sub-second. In production, longer chain confirmations and bank-rail latencies usually space the events out enough that ordering is incidental — but the contract still does not guarantee it. How to handle it:
  • Treat status as the source of truth, not the order of arrival. Each event carries the full resource state; the latest event by terminal-state semantics wins.
  • Use X-Dakota-Event-ID for idempotency — duplicate deliveries (retries or out-of-order replays) carry the same ID.
  • For ordered processing, derive a sequence from the resource’s own state machine. For wallet transactions: Waiting For Signature → Broadcasted → Success | Failed. For auto-account deposits: received → succeeded | failed. If you receive a “later” status before an “earlier” one, accept it; the earlier one is informational.
  • Don’t strictly require monotonic timestampscreated and X-Webhook-Timestamp are second-resolution, and two events can share the same value.
If your processing must be strictly serial per resource, queue incoming events by resource ID (e.g. wallet_id, transaction_id) and reorder by status before applying.

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.auto.updated)
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:
// 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.auto.created':
        await this.handleTransactionCreated(event.data);
        break;
      case 'transaction.auto.updated':
        // Inspect event.data.status to distinguish processing/completed/failed
        await this.handleTransactionUpdate(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');
  }
});

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.auto.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
customer.kyb_application.submittedCustomer KYB application submitted for review
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
Wallet Events
wallet.createdNon-custodial wallet created
wallet.updatedWallet metadata or configuration changed
wallet.depositOn-chain deposit detected at a wallet address
wallet.transaction.createdWallet transaction submitted
wallet.transaction.updatedWallet transaction status or details changed
wallet.policy.createdPolicy created and attached to a wallet
wallet.policy.updatedPolicy rules or metadata updated
wallet.signer_group.createdSigner group created
wallet.signer_group.updatedSigner group membership or metadata updated
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

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: