Transfer Webhooks
The MNEE SDK supports webhook callbacks for asynchronous transaction status updates. When you provide a `callbackUrl` in your transfer options, the MNEE API will send real-time status updates to your webhook endpoint as the transaction progresses through various states.
How It Works
When you initiate a transfer with a webhook URL, the API will:
Accept your transaction and return a
ticketId
immediatelyProcess the transaction asynchronously
Send POST requests to your webhook URL as the transaction status changes
Continue sending updates until the transaction reaches a final state (SUCCESS, MINED, or FAILED)
Webhook Response Format
Your webhook endpoint will receive a POST request with the following TransferWebhookResponse
payload:
{
id: string; // The ticket ID for this transaction
tx_id: string; // The blockchain transaction ID
tx_hex: string; // The raw transaction hex
action_requested: 'transfer'; // Always 'transfer' for MNEE transactions
callback_url: string; // Your webhook URL (for verification)
status: 'BROADCASTING' | 'SUCCESS' | 'MINED' | 'FAILED';
createdAt: string; // ISO timestamp when ticket was created
updatedAt: string; // ISO timestamp of this update
errors: string | null; // Error details if status is FAILED
}
Status Flow
Transactions typically progress through these states:
BROADCASTING → Transaction is being broadcast to the network
SUCCESS → Transaction successfully broadcast and accepted by the network
MINED → Transaction has been mined into a block
FAILED → Transaction failed (check
errors
field for details)
Usage Examples
Basic Transfer With Webhook
const options = {
inputs: [
{ txid: 'abc...', vout: 0, wif: 'wif1' },
{ txid: 'def...', vout: 1, wif: 'wif2' }
],
recipients: [
{ address: 'address1', amount: 5.0 },
{ address: 'address2', amount: 3.5 }
]
};
const response = await mnee.transferMulti(options, {
broadcast: true,
callbackUrl: 'https://your-api.com/webhook'
});
console.log('Multi-transfer submitted:', response.ticketId);
Submit Raw Transaction With Webhook
const rawTxHex = '0100000001...'; // Your signed transaction
const response = await mnee.submitRawTx(rawTxHex, {
broadcast: true,
callbackUrl: 'https://your-api.com/webhook'
});
console.log('Raw transaction submitted:', response.ticketId);
Implementing a Webhook Endpoint
Express.js Example
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
const webhookData = req.body;
console.log(`Transaction ${webhookData.id} status: ${webhookData.status}`);
switch (webhookData.status) {
case 'BROADCASTING':
// Transaction is being broadcast
await updateDatabase(webhookData.id, 'broadcasting');
break;
case 'SUCCESS':
// Transaction successfully broadcast
await updateDatabase(webhookData.id, 'success', webhookData.tx_id);
await notifyUser(webhookData.id, 'Your transaction has been broadcast!');
break;
case 'MINED':
// Transaction mined into a block
await updateDatabase(webhookData.id, 'confirmed', webhookData.tx_id);
await notifyUser(webhookData.id, 'Your transaction has been confirmed!');
break;
case 'FAILED':
// Transaction failed
await updateDatabase(webhookData.id, 'failed', null, webhookData.errors);
await notifyUser(webhookData.id, `Transaction failed: ${webhookData.errors}`);
break;
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Best Practices
1. Always Return 200 OK
Always return a 200 status code to acknowledge receipt, even if processing fails. This prevents the webhook from being retried unnecessarily.
app.post('/webhook', async (req, res) => {
try {
await processWebhook(req.body);
} catch (error) {
// Log error but still return 200
console.error('Webhook processing failed:', error);
}
res.status(200).json({ received: true });
});
2. Implement Idempotency
Webhooks may be sent multiple times for the same status. Design your handler to be idempotent:
async function processWebhook(data) {
// Check if we've already processed this update
const processed = await checkIfProcessed(data.id, data.status, data.updatedAt);
if (processed) {
console.log(`Already processed ${data.id} at status ${data.status}`);
return;
}
// Process the update
await updateTransactionStatus(data);
// Mark as processed
await markAsProcessed(data.id, data.status, data.updatedAt);
}
3. Handle Timeouts Gracefully
Set up fallback polling for critical transactions in case webhooks fail:
async function transferWithFallback(recipients, wif, webhookUrl) {
const response = await mnee.transfer(recipients, wif, {
broadcast: true,
callbackUrl: webhookUrl
});
// Set up fallback polling after 30 seconds
setTimeout(async () => {
const status = await mnee.getTxStatus(response.ticketId);
if (status.status === 'BROADCASTING') {
// Webhook might have failed, start polling
pollTransactionStatus(response.ticketId);
}
}, 30000);
return response;
}
4. Secure Your Endpoint
Implement security measures to protect your webhook endpoint:
// Use a secret path
app.post('/webhook/' + process.env.WEBHOOK_SECRET, handler);
// Implement rate limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100 // limit to 100 requests per minute
});
app.use('/webhook', limiter);
// Validate payload structure
function validateWebhookPayload(data) {
return (
typeof data.id === 'string' &&
typeof data.tx_id === 'string' &&
typeof data.status === 'string' &&
['BROADCASTING', 'SUCCESS', 'MINED', 'FAILED'].includes(data.status)
);
}
5. Queue For Processing
For high-volume applications, queue webhook payloads for async processing:
import Queue from 'bull';
const webhookQueue = new Queue('webhook-processing');
app.post('/webhook', async (req, res) => {
// Immediately queue for processing
await webhookQueue.add('process-webhook', req.body);
// Return immediately
res.status(200).json({ received: true });
});
// Process queue items
webhookQueue.process('process-webhook', async (job) => {
const webhookData = job.data;
await processWebhook(webhookData);
});
Testing Webhooks
Local Development with ngrok
For local testing, use ngrok to expose your local server:
# Install ngrok
npm install -g ngrok
# Start your local server on port 3000
npm run dev
# In another terminal, expose port 3000
ngrok http 3000
# Use the ngrok URL as your webhook
# https://abc123.ngrok.io/webhook
Test Webhook Server
Create a simple test server to log webhook calls:
// test-webhook-server.js
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.text());
// Log all webhooks
app.all('*', (req, res) => {
console.log('=== Webhook Received ===');
console.log('Method:', req.method);
console.log('Path:', req.path);
console.log('Headers:', req.headers);
console.log('Body:', req.body);
console.log('========================');
res.status(200).json({ received: true });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Test webhook server listening on port ${port}`);
});
Error Handling
Webhook Delivery Failures
If webhook delivery fails, you can still check transaction status using the ticket ID:
async function checkTransactionWithFallback(ticketId) {
try {
// Check if we received webhook updates
const webhookStatus = await getWebhookStatus(ticketId);
if (webhookStatus) {
return webhookStatus;
}
// Fall back to polling
console.log('No webhook received, polling status...');
const status = await mnee.getTxStatus(ticketId);
return status;
} catch (error) {
console.error('Failed to check transaction status:', error);
throw error;
}
}
Handling Failed Transactions
When a webhook indicates a failed transaction:
async function handleFailedTransaction(webhookData) {
// Log the failure
console.error(`Transaction ${webhookData.id} failed:`, webhookData.errors);
// Parse error for specific handling
if (webhookData.errors?.includes('Insufficient')) {
// Handle insufficient funds
await notifyUserInsufficientFunds(webhookData.id);
} else if (webhookData.errors?.includes('Invalid')) {
// Handle invalid transaction
await notifyUserInvalidTransaction(webhookData.id);
} else {
// Generic error handling
await notifyUserTransactionFailed(webhookData.id, webhookData.errors);
}
// Maybe retry with different parameters
if (shouldRetry(webhookData.errors)) {
await retryTransaction(webhookData.id);
}
}
Important Notes
Webhooks are only sent when
broadcast: true
and acallbackUrl
is providedThe webhook URL must be publicly accessible (not localhost unless using ngrok or similar)
Webhooks may arrive out of order - always check the
updatedAt
timestampMultiple webhooks may be sent for the same status - implement idempotency
Webhook delivery is not guaranteed - implement fallback polling for critical transactions
The
tx_id
field will be empty until the transaction reaches SUCCESS statusAlways respond quickly to webhooks (< 5 seconds) to avoid timeouts
See Also
Transfer - Create and broadcast transactions
Transfer Multi - Advanced transfers with multiple inputs
Submit Raw Transaction - Submit pre-signed transactions
Get Transaction Status - Poll for transaction status
Last updated