Back to Blog

Setting Up Webhooks for Real-Time Blockchain Verification

Configure webhooks to receive instant notifications when your records are anchored to the Polygon blockchain. Stop polling, start reacting.

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
Recommendation: Use webhooks in production. They're more efficient, provide instant notifications, and reduce API calls.

How Anchora Webhooks Work

Here's the complete webhook flow:

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:

JSON - Webhook 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:

JavaScript - Express.js Webhook Handler
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:

JavaScript - Anchor with Webhook
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:

JavaScript - Idempotent Handler
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 });
});
Important: Always return HTTP 200 to acknowledge receipt, even for duplicates. If you return an error code, Anchora will retry the webhook.

Viewing Webhook History

You can view your webhook delivery history and status via the API:

JavaScript - Get Webhook History
// 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:

JavaScript - Retry Failed Webhook
// 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:

JavaScript - Validate Webhook
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:

Terminal
# 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:

JavaScript - Complete System
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