Why Webhooks?
When you anchor a record with Anchora, it takes 30-60 seconds to be confirmed on the Polygon blockchain. You have two options to know when it's done:
| Approach | How It Works | Best For |
|---|---|---|
| Polling | Repeatedly call GET /v1/proof/:hash until status is ANCHORED |
Simple scripts, testing |
| Webhooks | Anchora calls YOUR endpoint when the record is anchored | Production apps, real-time UX |
How Anchora Webhooks Work
Here's the complete webhook flow:
1. You anchor a record with webhookUrl parameter
POST /v1/anchor { data: {...}, webhookUrl: "https://your-app.com/webhook" }
2. Anchora queues the record (status: QUEUED)
Response: { hash: "abc123...", status: "QUEUED" }
3. Worker batches records every 30 seconds (status: BATCHING)
- Collects up to 256 records
- Builds Merkle tree
- Submits to Polygon blockchain
4. Blockchain confirms (status: ANCHORED)
- Block mined on Polygon (5-10 seconds)
- Transaction confirmed
5. Anchora sends webhook to YOUR endpoint
POST https://your-app.com/webhook
{
"event": "record.anchored",
"hash": "abc123...",
"blockNumber": 45123789,
...
}
6. Your app updates its database with the proof
Webhook Payload Structure
When your record is anchored, Anchora sends a POST request to your webhook URL with this payload:
{
"event": "record.anchored",
"recordId": "674e8f2a1b3c4d5e6f7a8b9c",
"hash": "3a7bd3e259c8f2e1d4b9a6f1c3e5d7b2a1f4e6c8d9b7a5f3e1c2d4b6a8f0e2d4",
"status": "ANCHORED",
"blockNumber": 45123789,
"transactionHash": "0xb706858a2c9e4a8e3d5f9c1a2b3d4e5f6789abcdef...",
"merkleProof": [
"0x1234567890abcdef...",
"0xabcdef1234567890..."
],
"merkleRoot": "0x9876543210fedcba...",
"batchId": 30,
"anchoredAt": "2026-01-25T10:01:00.000Z",
"collection": "certificates",
"metadata": {
"issuer": "MIT",
"type": "degree"
}
}
Payload Fields Explained
| Field | Type | Description |
|---|---|---|
event |
string | Event type (currently always "record.anchored") |
recordId |
string | Unique identifier for the record in Anchora |
hash |
string | SHA-256 hash of your original data |
blockNumber |
number | Polygon block number where the Merkle root was anchored |
transactionHash |
string | Blockchain transaction hash (viewable on Polygonscan) |
merkleProof |
array | Cryptographic proof path from your hash to the Merkle root |
batchId |
number | The batch this record was included in |
anchoredAt |
ISO date | Timestamp when the record was confirmed on blockchain |
Step 1: Create Your Webhook Endpoint
Create an endpoint in your application to receive webhook notifications. Here's an example using Express.js:
const express = require('express');
const app = express();
// Parse JSON bodies
app.use(express.json());
// Webhook endpoint for Anchora notifications
app.post('/webhooks/anchora', async (req, res) => {
const {
event,
recordId,
hash,
status,
blockNumber,
transactionHash,
merkleProof,
anchoredAt,
collection
} = req.body;
console.log(`Webhook received: ${event} for record ${recordId}`);
if (event === 'record.anchored') {
try {
// Update your database with the blockchain proof
await db.records.updateOne(
{ 'anchora.hash': hash },
{
$set: {
'anchora.status': status,
'anchora.blockNumber': blockNumber,
'anchora.transactionHash': transactionHash,
'anchora.merkleProof': merkleProof,
'anchora.anchoredAt': new Date(anchoredAt)
}
}
);
console.log(`Record ${recordId} anchored at block ${blockNumber}`);
// Optional: Notify user, trigger downstream processes, etc.
await notifyUser(recordId, blockNumber);
} catch (error) {
console.error('Error processing webhook:', error);
// Return 500 to trigger retry
return res.status(500).json({ error: 'Processing failed' });
}
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Step 2: Anchor with Webhook URL
When anchoring records, include the webhookUrl parameter:
const response = await fetch('https://api.anchora.io/v1/anchor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ANCHORA_API_KEY}`
},
body: JSON.stringify({
data: {
certificateId: 'CERT-2026-001',
studentName: 'Alice Johnson',
degree: 'Computer Science',
graduationDate: '2026-05-15'
},
collection: 'certificates',
metadata: {
issuer: 'MIT',
type: 'degree'
},
// Add your webhook URL here
webhookUrl: 'https://your-app.com/webhooks/anchora'
})
});
const result = await response.json();
console.log('Record queued:', result.hash);
// You'll receive a webhook when it's anchored!
Webhook Retry Logic
Anchora automatically retries failed webhooks with exponential backoff. If your endpoint is temporarily unavailable, we'll keep trying:
| Retry | Delay | Total Time Elapsed |
|---|---|---|
| 1st retry | 1 second | 1s |
| 2nd retry | 2 seconds | 3s |
| 3rd retry | 4 seconds | 7s |
| 4th retry | 8 seconds | 15s |
| 5th retry (final) | 16 seconds | 31s |
After 5 failed attempts, the webhook is marked as FAILED. You can manually retry it via the API.
Making Webhooks Idempotent
Your webhook handler may receive the same event multiple times (network issues, retries, etc.). Always make your handlers idempotent:
app.post('/webhooks/anchora', async (req, res) => {
const { recordId, hash } = req.body;
// Check if we've already processed this webhook
const existing = await db.webhookLogs.findOne({ recordId });
if (existing) {
console.log(`Webhook for ${recordId} already processed, skipping`);
return res.status(200).json({ received: true, duplicate: true });
}
// Process the webhook
await processWebhook(req.body);
// Log that we processed it
await db.webhookLogs.create({
recordId,
hash,
processedAt: new Date()
});
res.status(200).json({ received: true });
});
Viewing Webhook History
You can view your webhook delivery history and status via the API:
// List webhook delivery history
const response = await fetch('https://api.anchora.io/v1/webhooks?status=FAILED&limit=50', {
headers: {
'Authorization': `Bearer ${process.env.ANCHORA_API_KEY}`
}
});
const result = await response.json();
console.log('Failed webhooks:', result.data);
console.log('Summary:', result.summary);
// {
// PENDING: 10,
// SENT: 2800,
// FAILED: 5
// }
Retrying Failed Webhooks
If a webhook failed (max 5 retries exhausted), you can manually retry it:
// Retry a specific failed webhook
const recordId = '674e8f2a1b3c4d5e6f7a8b9c';
const response = await fetch(`https://api.anchora.io/v1/webhooks/${recordId}/retry`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ANCHORA_API_KEY}`
}
});
const result = await response.json();
if (result.success) {
console.log('Webhook retried successfully!');
console.log('New status:', result.webhookStatus); // SENT
} else {
console.log('Retry failed:', result.message);
}
Security Best Practices
Secure your webhook endpoint to prevent unauthorized requests:
1. Use HTTPS Only
Anchora only sends webhooks to HTTPS URLs. Never use HTTP in production.
2. Validate the Payload
Verify the webhook data by checking if the hash exists in your records:
app.post('/webhooks/anchora', async (req, res) => {
const { hash, recordId } = req.body;
// Verify this hash exists in your database
const record = await db.records.findOne({ 'anchora.hash': hash });
if (!record) {
console.warn(`Unknown hash received: ${hash}`);
return res.status(400).json({ error: 'Unknown record' });
}
// Process the legitimate webhook
await processWebhook(req.body);
res.status(200).json({ received: true });
});
3. IP Whitelisting (Optional)
For additional security, you can whitelist Anchora's IP addresses. Contact support for the current list.
Testing Webhooks Locally
Use tools like ngrok to expose your local server for testing:
# Start your local server
node server.js # Running on port 3000
# In another terminal, start ngrok
ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
# Use the ngrok URL as your webhookUrl:
# webhookUrl: "https://abc123.ngrok.io/webhooks/anchora"
Complete Example: Certificate System with Webhooks
Here's a complete example of a certificate issuance system using webhooks:
const express = require('express');
const app = express();
app.use(express.json());
// Issue a new certificate
app.post('/certificates', async (req, res) => {
const certificate = {
id: generateId(),
studentName: req.body.studentName,
degree: req.body.degree,
issuedAt: new Date().toISOString()
};
// Anchor to blockchain with webhook
const anchorResult = await fetch('https://api.anchora.io/v1/anchor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ANCHORA_API_KEY}`
},
body: JSON.stringify({
data: certificate,
collection: 'certificates',
webhookUrl: `${process.env.APP_URL}/webhooks/anchora`
})
}).then(r => r.json());
// Save certificate with pending anchor status
await db.certificates.create({
...certificate,
anchora: {
hash: anchorResult.hash,
recordId: anchorResult.recordId,
status: 'QUEUED' // Will be updated by webhook
}
});
res.json({
certificate,
message: 'Certificate issued! Blockchain proof pending.'
});
});
// Webhook handler
app.post('/webhooks/anchora', async (req, res) => {
const { hash, blockNumber, transactionHash, merkleProof, anchoredAt } = req.body;
// Update certificate with blockchain proof
await db.certificates.updateOne(
{ 'anchora.hash': hash },
{
$set: {
'anchora.status': 'ANCHORED',
'anchora.blockNumber': blockNumber,
'anchora.transactionHash': transactionHash,
'anchora.merkleProof': merkleProof,
'anchora.anchoredAt': anchoredAt
}
}
);
// Notify the student
const cert = await db.certificates.findOne({ 'anchora.hash': hash });
await sendEmail(cert.studentEmail, {
subject: 'Your certificate is now blockchain-verified!',
body: `View proof: https://polygonscan.com/tx/${transactionHash}`
});
res.status(200).json({ received: true });
});
// Verify a certificate
app.get('/certificates/:id/verify', async (req, res) => {
const cert = await db.certificates.findOne({ id: req.params.id });
if (cert.anchora.status !== 'ANCHORED') {
return res.json({ verified: false, message: 'Pending blockchain confirmation' });
}
res.json({
verified: true,
certificate: cert,
blockchainProof: {
blockNumber: cert.anchora.blockNumber,
transactionHash: cert.anchora.transactionHash,
explorerUrl: `https://polygonscan.com/tx/${cert.anchora.transactionHash}`
}
});
});
app.listen(3000);
Summary
Webhooks are the recommended way to receive blockchain confirmation notifications from Anchora. Key points:
- Add webhookUrl when anchoring records
- Return HTTP 200 to acknowledge receipt
- Make handlers idempotent to handle duplicates
- Automatic retries with exponential backoff (up to 5 attempts)
- Manual retry API for failed webhooks
- Use HTTPS only in production
Ready to implement real-time verification?
Start receiving instant blockchain notifications today. Free tier includes 10,000 records/month.
Get Your API Key