Best Practices
Respond quickly
Your webhook endpoint should:
- Respond with HTTP 200 within 10 seconds
- Process webhooks asynchronously (queue for background processing)
- Avoid long-running operations before responding
Example pattern:
app.post('/webhooks/minteo', async (req, res) => { const event = req.body;
// 1. Validate signature (fast) if (!validateChecksum(event, secret)) { return res.status(401).send('Unauthorized'); }
// 2. Queue for background processing await queue.add('process-webhook', event);
// 3. Respond immediately res.status(200).send('OK');
// 4. Process in background worker (not blocking the response)});Implement idempotency
The same event may be delivered multiple times due to retries. Use event_id to deduplicate.
Example:
async function processWebhook(event: WebhookEvent) { const { event_id, event_type, data } = event;
// Check if already processed const exists = await db.processedEvents.findOne({ event_id }); if (exists) { console.log(`Event ${event_id} already processed`); return; }
// Process the event await handleEvent(event_type, data);
// Mark as processed await db.processedEvents.insert({ event_id, processed_at: new Date() });}Use event_id for deduplication, not hook_id. The hook_id changes with each retry attempt, but event_id remains constant for the same business event.
Verify signatures on every request
Always verify the webhook signature before processing. This prevents:
- Forged requests from malicious actors
- Accidental processing of invalid data
- Replay attacks
See Verifying Signatures for implementation details.
Use HTTPS endpoints
Your webhook URL must use HTTPS. HTTP endpoints are not supported.
Handle failures gracefully
If your endpoint experiences an error:
- Return a non-200 status code to trigger a retry
- Log the error for debugging
- Minteo will retry up to 14 times over ~64 hours
Example:
app.post('/webhooks/minteo', async (req, res) => { try { const event = req.body;
if (!validateChecksum(event, secret)) { return res.status(401).send('Unauthorized'); }
await processWebhook(event); res.status(200).send('OK'); } catch (error) { console.error('Webhook processing failed:', error); // Return 500 to trigger retry res.status(500).send('Internal Server Error'); }});Monitor webhook delivery
Check the webhook history in Board to monitor:
- Delivery success rates
- Failed webhooks
- Retry attempts
- Production
- Sandbox
View history at: https://board.minteo.com/developers/history
View history at: https://board.sandbox.minteo.com/developers/history
Testing webhooks
Sandbox testing
Test webhooks in Sandbox by performing real operations:
| Event | How to trigger | Final states |
|---|---|---|
order.updated | Create an order | SUCCEEDED, FAILED |
payout.updated | Create a payout | FULFILLED, ABORTED |
payin.updated | Create a payin | FULFILLED, ABORTED |
payout.item.updated | Create a payout with items | SUCCEEDED, REJECTED |
For payin.updated, you'll also receive a webhook when the gateway confirms payment (GATEWAY_CONFIRM_PAYIN).
Manual webhook triggering is not currently supported. You must perform actual operations to generate webhook events.
Local development
For local development, expose your localhost as an HTTPS endpoint using ngrok:
# Install ngroknpm install -g ngrok
# Expose local port 3000ngrok http 3000
# Use the HTTPS URL in Board webhook configuration# Example: https://abc123.ngrok.io/webhooks/minteoRemember to update your webhook URL in Board when switching between local development and deployed environments.