Files
zeekay 4c917f849b Move every caller and route to /v1
Commerce is called at /v1, the indexer at /v1/indexer, and the operator
serves /v1/clusters. The console's unused fleet helpers called a /api
base nothing serves; they are gone. The legacy Quart env files named an
app that no longer exists.

Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-29 18:34:58 -07:00

401 lines
15 KiB
TypeScript

"use client"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { DocsLayout } from "@/components/docs-layout"
import { ArrowRight } from "@luxfi/ui/icons"
import { docsConfig } from "@/lib/docs-config"
export default function WebhooksQuickstartPage() {
return (
<DocsLayout>
<div className="space-y-10">
{/* Header */}
<div>
<h1 className="text-4xl font-bold mb-4">Webhooks Quickstart</h1>
<p className="text-lg text-muted-foreground">
Receive real-time notifications when on-chain events happen. No polling required.
{docsConfig.brandName} will POST a signed JSON payload to your endpoint whenever a matching event
occurs.
</p>
</div>
{/* Event Types */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Supported Event Types</h2>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-3 font-medium">Event Type</th>
<th className="text-left p-3 font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-xs">address_activity</td>
<td className="p-3 text-muted-foreground">Any transaction involving specified addresses (sends, receives, contract interactions)</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-xs">token_transfer</td>
<td className="p-3 text-muted-foreground">ERC-20 token transfers to/from specified addresses or contracts</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-xs">nft_transfer</td>
<td className="p-3 text-muted-foreground">ERC-721 and ERC-1155 NFT transfers</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-xs">mined_transaction</td>
<td className="p-3 text-muted-foreground">Specific transaction hash has been mined and confirmed</td>
</tr>
<tr>
<td className="p-3 font-mono text-xs">log</td>
<td className="p-3 text-muted-foreground">Contract event logs matching specific topics and addresses</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Step 1 */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<Badge className="rounded-full h-8 w-8 flex items-center justify-center text-sm">1</Badge>
<h2 className="text-2xl font-semibold">Create a Webhook Subscription</h2>
</div>
<p className="text-muted-foreground">
Register your endpoint and specify which events you want to receive.
{docsConfig.brandName} returns a <code className="bg-muted px-1.5 py-0.5 rounded text-sm">signing_secret</code>{" "}
that you will use to verify webhook payloads.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`curl -X POST ${docsConfig.apiUrl}/v1/webhooks \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
"addresses": [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
]
}
}'`}</code>
</pre>
<p className="text-sm text-muted-foreground">Response:</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`{
"id": "wh_a1b2c3d4e5f6",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
"addresses": [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
]
},
"signing_secret": "whsec_k7m9p2q4r6s8t0v2w4x6y8z0",
"status": "active",
"created_at": "2026-01-15T10:30:00Z"
}`}</code>
</pre>
<Card>
<CardContent className="pt-6">
<p className="text-sm font-medium">Important</p>
<p className="text-sm text-muted-foreground mt-1">
Save the <code className="bg-muted px-1.5 py-0.5 rounded text-xs">signing_secret</code> securely.
It is only returned once during creation. You will need it to verify incoming webhook signatures.
</p>
</CardContent>
</Card>
</section>
{/* Step 2 */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<Badge className="rounded-full h-8 w-8 flex items-center justify-center text-sm">2</Badge>
<h2 className="text-2xl font-semibold">Set Up Your Webhook Endpoint</h2>
</div>
<p className="text-muted-foreground">
Create an Express.js server (or any HTTP server) to receive webhook events.
{docsConfig.brandName} sends POST requests with a JSON body and an HMAC signature in the headers.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// server.ts
import express from "express";
import crypto from "crypto";
const app = express();
const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET!;
// "whsec_k7m9p2q4r6s8t0v2w4x6y8z0"
// IMPORTANT: Use raw body for signature verification
app.post(
"/webhooks/bootnode",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-bootnode-signature"] as string;
const timestamp = req.headers["x-bootnode-timestamp"] as string;
const body = req.body as Buffer;
// 1. Verify the signature
if (!verifySignature(body, signature, timestamp)) {
console.error("Invalid webhook signature");
return res.status(401).json({ error: "Invalid signature" });
}
// 2. Parse the event
const event = JSON.parse(body.toString());
console.log("Received event:", event.event_type, event.data);
// 3. Handle the event
switch (event.event_type) {
case "address_activity":
handleAddressActivity(event.data);
break;
case "token_transfer":
handleTokenTransfer(event.data);
break;
case "nft_transfer":
handleNftTransfer(event.data);
break;
default:
console.log("Unhandled event type:", event.event_type);
}
// 4. Return 200 to acknowledge receipt
res.status(200).json({ received: true });
}
);
app.listen(3001, () => {
console.log("Webhook server listening on port 3001");
});`}</code>
</pre>
</section>
{/* Step 3 */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<Badge className="rounded-full h-8 w-8 flex items-center justify-center text-sm">3</Badge>
<h2 className="text-2xl font-semibold">Verify the HMAC Signature</h2>
</div>
<p className="text-muted-foreground">
Every webhook request includes two headers for verification:{" "}
<code className="bg-muted px-1.5 py-0.5 rounded text-sm">X-Bootnode-Signature</code> and{" "}
<code className="bg-muted px-1.5 py-0.5 rounded text-sm">X-Bootnode-Timestamp</code>.
The signature is a HMAC-SHA256 hex digest of{" "}
<code className="bg-muted px-1.5 py-0.5 rounded text-sm">{`{timestamp}.{body}`}</code>.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`function verifySignature(
body: Buffer,
signature: string,
timestamp: string
): boolean {
// 1. Reject old timestamps (prevent replay attacks)
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(timestamp, 10);
if (Math.abs(now - ts) > 300) {
// 5-minute tolerance
return false;
}
// 2. Compute expected signature
const payload = \`\${timestamp}.\${body.toString()}\`;
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(payload)
.digest("hex");
// 3. Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}`}</code>
</pre>
</section>
{/* Step 4 */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<Badge className="rounded-full h-8 w-8 flex items-center justify-center text-sm">4</Badge>
<h2 className="text-2xl font-semibold">Handle Event Types</h2>
</div>
<p className="text-muted-foreground">
Each event type has a specific payload structure. Here are handlers for the
most common event types:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`interface AddressActivityEvent {
chain: string;
block_number: number;
tx_hash: string;
from: string;
to: string;
value: string; // wei as decimal string
asset: string; // "ETH" or token symbol
contract: string | null; // null for native transfers
direction: "incoming" | "outgoing";
}
function handleAddressActivity(data: AddressActivityEvent) {
console.log(
\`[\${data.chain}] \${data.direction} \${data.asset}: \` +
\`\${data.from} -> \${data.to} (\${data.value} wei)\`
);
// Example: notify user of incoming transfer
if (data.direction === "incoming") {
notifyUser(data.to, \`Received \${data.asset} from \${data.from}\`);
}
}
interface TokenTransferEvent {
chain: string;
block_number: number;
tx_hash: string;
contract: string;
from: string;
to: string;
value: string;
symbol: string;
decimals: number;
}
function handleTokenTransfer(data: TokenTransferEvent) {
const formatted = (
Number(BigInt(data.value)) / Math.pow(10, data.decimals)
).toFixed(data.decimals);
console.log(
\`[\${data.chain}] Token transfer: \${formatted} \${data.symbol}\`
);
}
interface NftTransferEvent {
chain: string;
block_number: number;
tx_hash: string;
contract: string;
from: string;
to: string;
token_id: string;
standard: "ERC721" | "ERC1155";
quantity: number; // always 1 for ERC721
}
function handleNftTransfer(data: NftTransferEvent) {
console.log(
\`[\${data.chain}] NFT transfer: #\${data.token_id} \` +
\`(\${data.standard}) from \${data.from} to \${data.to}\`
);
}
function notifyUser(address: string, message: string) {
// Your notification logic (email, push, in-app, etc.)
console.log(\`Notify \${address}: \${message}\`);
}`}</code>
</pre>
</section>
{/* Example Payload */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Example Webhook Payload</h2>
<p className="text-muted-foreground">
Here is a full example of what {docsConfig.brandName} sends to your endpoint:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// Headers:
// X-Bootnode-Signature: 5a2f8c9d1e3b4a6f7c8d9e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
// X-Bootnode-Timestamp: 1706400000
// Content-Type: application/json
// Body:
{
"id": "evt_z9y8x7w6v5u4",
"event_type": "address_activity",
"webhook_id": "wh_a1b2c3d4e5f6",
"created_at": "2026-01-15T10:35:22Z",
"data": {
"chain": "ethereum",
"block_number": 20574335,
"tx_hash": "0xabc123def456789...",
"from": "0x1234567890abcdef1234567890abcdef12345678",
"to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"value": "1000000000000000000",
"asset": "ETH",
"contract": null,
"direction": "incoming"
}
}`}</code>
</pre>
</section>
{/* Retry Policy */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Retry Policy</h2>
<Card>
<CardContent className="pt-6 space-y-3">
<p className="text-sm text-muted-foreground">
If your endpoint returns a non-2xx status code or does not respond within 15 seconds,
{docsConfig.brandName} retries the delivery with exponential backoff:
</p>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-3 font-medium">Attempt</th>
<th className="text-left p-3 font-medium">Delay</th>
</tr>
</thead>
<tbody>
<tr className="border-b"><td className="p-3">1st retry</td><td className="p-3">10 seconds</td></tr>
<tr className="border-b"><td className="p-3">2nd retry</td><td className="p-3">1 minute</td></tr>
<tr className="border-b"><td className="p-3">3rd retry</td><td className="p-3">10 minutes</td></tr>
<tr className="border-b"><td className="p-3">4th retry</td><td className="p-3">1 hour</td></tr>
<tr><td className="p-3">5th retry</td><td className="p-3">6 hours</td></tr>
</tbody>
</table>
</div>
<p className="text-sm text-muted-foreground">
After 5 failed retries, the webhook is paused and you receive an email notification.
You can resume it from the dashboard or via the PATCH endpoint.
</p>
</CardContent>
</Card>
</section>
{/* Next Steps */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Next Steps</h2>
<div className="grid gap-3 sm:grid-cols-2">
{[
{ title: "API Reference", href: "/docs/api", desc: "Full webhook API endpoints" },
{ title: "WebSockets", href: "/docs/websockets/quickstart", desc: "Real-time streaming alternative" },
{ title: "Smart Wallets", href: "/docs/wallets/quickstart", desc: "Monitor smart wallet activity" },
{ title: "Examples", href: "/docs/examples", desc: "More webhook use cases" },
].map((item) => (
<Link
key={item.title}
href={item.href}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted transition-colors group"
>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</Link>
))}
</div>
</section>
</div>
</DocsLayout>
)
}