Webhook Best Practices
Webhooks are the recommended way to track transaction status in Neutron. This guide explains why webhooks outperform polling, how to set them up, and how to handle edge cases.
Prerequisites
Before using any Neutron integration (API, SDK, or MCP), you need an API key. You get this from the dashboard at portal.neutron.me.
- Sign up at portal.neutron.me
- Access your dashboard
- Generate your API key and secret
Everything can be configured, but the API key is the final piece — without it, wallet services won't function.
Note: Keep your API secret secure. Never commit it to version control or expose it in client-side code.
Why Webhooks Over Polling?
| Webhooks | Polling | |
|---|---|---|
| Latency | Sub-second | 1–30s depending on interval |
| Rate limits | Not affected | Consumes your quota |
| Reliability | Push delivery with retries | You manage retry logic |
| Complexity | One-time setup | Ongoing loop to maintain |
Polling the /api/v2/transaction/{txnId} endpoint works, but burns through your rate limit allowance quickly — especially for high-volume integrations. A busy integration polling every 2 seconds for 10 concurrent transactions makes 300 requests per minute. Webhooks deliver the same information in a single push.
Rate Limit Reference
| Endpoint | Limit |
|---|---|
GET /api/v2/transaction/:id | 60 req/min per API key |
GET /api/v2/transaction | 30 req/min per API key |
POST /api/v2/webhook | 10 req/min per API key |
Once you hit the limit, you'll receive 429 Too Many Requests responses until the window resets.
Setting Up Webhooks
Via the API
curl -X POST https://api.neutron.me/api/v2/webhook \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"callback": "https://yourapp.com/webhooks/neutron",
"secret": "your-webhook-secret"
}'Store the webhook secret securely — you'll use it to verify incoming payloads.
Via the SDK
const { Neutron } = require('neutron-sdk');
const neutron = new Neutron({
apiKey: process.env.NEUTRON_API_KEY,
apiSecret: process.env.NEUTRON_API_SECRET,
});
const webhook = await neutron.webhooks.create({
callback: 'https://yourapp.com/webhooks/neutron',
secret: process.env.WEBHOOK_SECRET,
});
console.log('Webhook created:', webhook.webhookId);Via the MCP Tool (AI Agents)
Use the neutron_create_webhook tool:
callback: "https://yourapp.com/webhooks/neutron"
secret: "your-webhook-secret"
Webhook Payload
When a transaction status changes, Neutron POSTs to your callback URL:
{
"txnId": "41866d80-a46a-4845-a205-5cffc881cad3",
"extRefId": "order-abc1234",
"txnState": "completed",
"msg": "",
"updatedAt": 1676030467492
}Transaction states:
quoted— Created, awaiting confirmationprocessing— Confirmed, payment in flightcompleted— Payment successfulfailed— Payment failedexpired— Invoice or transaction timed out
Verifying Webhook Signatures
Always verify the signature. Without verification, anyone can POST fake events to your endpoint.
Neutron signs every webhook payload with HMAC-SHA256 using your webhook secret. The signature is sent in the X-Neutronpay-Signature header.
Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/neutron', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-neutronpay-signature'];
const secret = process.env.WEBHOOK_SECRET;
// Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(req.body) // raw body bytes, not parsed JSON
.digest('hex');
// Timing-safe comparison
const valid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
if (!valid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
console.log('Verified event:', event.txnId, event.txnState);
// Handle the event
if (event.txnState === 'completed') {
// Fulfill order, release funds, etc.
}
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/neutron', methods=['POST'])
def webhook():
signature = request.headers.get('X-Neutronpay-Signature', '')
secret = os.environ['WEBHOOK_SECRET'].encode()
expected = hmac.new(secret, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.json
print(f"Event: {event['txnId']} → {event['txnState']}")
return {'received': True}, 200Via the SDK
const { Neutron } = require('neutron-sdk');
// In your webhook handler:
app.post('/webhooks/neutron', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = Neutron.webhooks.verify(
req.body, // raw body
req.headers['x-neutronpay-signature'], // signature header
process.env.WEBHOOK_SECRET // your secret
);
if (!isValid) return res.status(401).end();
const event = JSON.parse(req.body);
// handle event...
res.status(200).json({ received: true });
});Responding to Webhooks
Your endpoint must:
- Return
200 OKwithin 10 seconds - Process asynchronously — queue the event and return immediately if processing takes time
- Be idempotent — you may receive the same event more than once
app.post('/webhooks/neutron', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
const event = JSON.parse(req.body);
await queue.push(event); // handle in background
});If your endpoint returns a non-200 status or times out, Neutron will retry delivery with exponential backoff.
Fallback: SSE for Ephemeral Agents
Some AI agents and serverless functions don't have a public URL to receive webhook POSTs. For these cases, use Server-Sent Events (SSE) to stream status updates directly.
Via the MCP Tool
Use neutron_subscribe_status:
transactionId: "txn-abc123"
timeoutSeconds: 60
The tool connects to the Neutron SSE stream and returns when the transaction reaches a terminal state (completed, failed, expired).
Via the SDK
// Stream status updates until terminal state
for await (const event of neutron.transactions.subscribe('txn-abc123')) {
console.log('Status update:', event.status);
if (['completed', 'failed', 'expired'].includes(event.status)) {
break;
}
}When to Use SSE vs Webhooks
| Scenario | Recommended |
|---|---|
| Long-running server | Webhooks |
| Serverless / Lambda | Webhooks (with public URL) |
| Ephemeral AI agent | SSE |
| Local development / testing | SSE |
| High volume (>100 tx/min) | Webhooks |
Checklist
- API key obtained from portal.neutron.me
- Webhook endpoint returns
200within 10 seconds - Signature verification implemented with
timingSafeEqual - Raw request body used for signature (not re-serialized JSON)
- Idempotency handled (same event may arrive twice)
- Webhook secret stored in env var, not hardcoded
- Fallback to SSE for agents without public endpoints
Updated 10 days ago
