Skip to main content

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() });
}
info

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

Testing webhooks

Sandbox testing

Test webhooks in Sandbox by performing real operations:

  • order.updated: Create an order and wait for it to reach SUCCEEDED/FAILED
  • payout.updated: Create a payout and wait for it to reach FULFILLED/ABORTED
  • payin.updated: Create a payin - you'll receive webhooks when the gateway confirms payment (GATEWAY_CONFIRM_PAYIN) and when it reaches final states (FULFILLED/ABORTED)
  • payout.item.updated: Create a payout item and wait for it to reach SUCCEEDED/REJECTED
info

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 ngrok
npm install -g ngrok

# Expose local port 3000
ngrok http 3000

# Use the HTTPS URL in Board webhook configuration
# Example: https://abc123.ngrok.io/webhooks/minteo
warning

Remember to update your webhook URL in Board when switching between local development and deployed environments.

Next steps