DID.COMMSDOCS

Webhook Recipes

Production-ready integration patterns for connecting Arcform webhook events to popular platforms. Each recipe includes signature verification, error handling, and the complete handler logic.

Arcform → Slack

Forward envelope alerts to a Slack channel in real time

Arcform → Shopify

Verify sender identity before clearing orders for fulfilment

Arcform → Python

Generic Flask handler with signature verification and proof validation

Arcform → XRPL

Anchor envelope hashes on-ledger via Xaman wallet

Prerequisites

✓ An active Arcform API key (activate via /v1/did/activate)
✓ A registered webhook endpoint (register via /v1/webhooks/register)
✓ Your whsec_ webhook secret stored securely in environment variables
✓ Node.js 18+ or Python 3.8+ runtime

Arcform → Slack

Forward sealed envelope alerts to a Slack channel with rich formatting

Node.js / Express

Forward sealed envelope alerts to a Slack channel with rich formatting

// Arcform → Slack: Forward envelope alerts to a channel
// Requires: express, crypto, @slack/web-api

const express = require('express');
const crypto = require('crypto');
const { WebClient } = require('@slack/web-api');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.DIDCOMMS_WEBHOOK_SECRET;
const slack = new WebClient(process.env.SLACK_TOKEN);
const CHANNEL = '#arcform-alerts';

function verifySignature(payload, signature) {
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  hmac.update(JSON.stringify(payload));
  return hmac.digest('hex') === signature;
}

app.post('/webhooks/arcform', async (req, res) => {
  const sig = req.headers['x-didcomms-signature'];
  if (!verifySignature(req.body, sig)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, envelope_hash, sender, metadata } = req.body;

  await slack.chat.postMessage({
    channel: CHANNEL,
    text: `:incoming_envelope: *Sealed Envelope Received*
• Hash: \`${envelope_hash}\`
• Sender: \`${sender}\`
• Size: ${metadata.payload_size_bytes} bytes
• Event: ${event}`,
  });

  res.json({ ok: true });
});

app.listen(3001, () => console.log('Webhook handler on :3001'));

Arcform → Shopify

Verify sender identity via proof-of-transit before order fulfilment

Node.js / Express

Verify sender identity via proof-of-transit before order fulfilment

// Arcform → Shopify: Verify order identity before fulfilment
// Requires: express, crypto, node-fetch

const express = require('express');
const crypto = require('crypto');
const fetch = require('node-fetch');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.DIDCOMMS_WEBHOOK_SECRET;
const ARCFORM_API = 'https://arcform-api.com';

app.post('/webhooks/arcform-order', async (req, res) => {
  const sig = req.headers['x-didcomms-signature'];
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  hmac.update(JSON.stringify(req.body));
  if (hmac.digest('hex') !== sig) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { envelope_hash } = req.body;

  // Step 1: Verify the envelope via Arcform
  const proof = await fetch(`${ARCFORM_API}/v1/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ envelope_hash }),
  }).then(r => r.json());

  // Step 2: Check identity verdict
  if (proof.identity?.identity_verdict !== 'ok') {
    console.error('Identity verification failed:', proof);
    // Flag order for manual review in Shopify
    return res.json({ action: 'hold', reason: 'identity_unverified' });
  }

  // Step 3: Proceed with fulfilment
  console.log('Identity verified — clearing order for fulfilment');
  res.json({ action: 'fulfil', proof_checksum: proof.proof_checksum });
});

app.listen(3002, () => console.log('Shopify handler on :3002'));

Arcform → Python (Flask)

Generic webhook handler with signature verification and proof validation

Python / Flask

Generic webhook handler with signature verification and proof validation

# Arcform → Python: Generic webhook handler with Flask
# Requires: flask, requests

from flask import Flask, request, jsonify
import hashlib, hmac, os, requests

app = Flask(__name__)

WEBHOOK_SECRET = os.environ['DIDCOMMS_WEBHOOK_SECRET']
ARCFORM_API = 'https://arcform-api.com'

def verify_signature(payload: bytes, signature: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhooks/arcform', methods=['POST'])
def handle_webhook():
    sig = request.headers.get('X-DIDComms-Signature', '')
    if not verify_signature(request.data, sig):
        return jsonify({'error': 'Invalid signature'}), 401

    data = request.json
    envelope_hash = data['envelope_hash']

    # Verify integrity via Arcform
    proof = requests.post(
        f'{ARCFORM_API}/v1/verify',
        json={'envelope_hash': envelope_hash},
    ).json()

    if proof.get('status') != 'verified':
        return jsonify({'action': 'reject', 'reason': 'unverified'}), 400

    # Your business logic here
    print(f"Verified envelope {envelope_hash}")
    print(f"  Identity verdict: {proof['identity']['identity_verdict']}")
    print(f"  Proof checksum:   {proof['proof_checksum']}")

    return jsonify({
        'action': 'accepted',
        'proof_checksum': proof['proof_checksum'],
    })

if __name__ == '__main__':
    app.run(port=3004, debug=True)

Arcform → XRPL Ledger

Anchor envelope hashes on the XRP Ledger as immutable memo transactions

Node.js / xrpl.js

Anchor envelope hashes on the XRP Ledger as immutable memo transactions

// Arcform → Xaman Wallet: Anchor envelope hash on XRPL
// Requires: express, crypto, xrpl

const express = require('express');
const crypto = require('crypto');
const xrpl = require('xrpl');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.DIDCOMMS_WEBHOOK_SECRET;

app.post('/webhooks/arcform-anchor', async (req, res) => {
  const sig = req.headers['x-didcomms-signature'];
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  hmac.update(JSON.stringify(req.body));
  if (hmac.digest('hex') !== sig) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { envelope_hash, metadata } = req.body;

  // Connect to XRPL and submit a memo transaction
  const client = new xrpl.Client('wss://s1.ripple.com');
  await client.connect();

  const wallet = xrpl.Wallet.fromSeed(process.env.XRPL_SEED);

  const tx = await client.submitAndWait({
    TransactionType: 'Payment',
    Account: wallet.address,
    Destination: wallet.address,  // self-payment for memo
    Amount: '1',                  // 1 drop
    Memos: [{
      Memo: {
        MemoType: Buffer.from('arcform/envelope-hash').toString('hex'),
        MemoData: Buffer.from(envelope_hash).toString('hex'),
      }
    }],
  }, { wallet });

  console.log('Anchored on XRPL:', tx.result.hash);
  await client.disconnect();

  res.json({ anchored: true, xrpl_hash: tx.result.hash });
});

app.listen(3003, () => console.log('XRPL anchor handler on :3003'));

Building Your Own Recipe

1

Verify the signature

Always validate X-DIDComms-Signature using HMAC-SHA256 with your whsec_ secret before processing any payload.

2

Extract envelope metadata

The webhook payload contains envelope_hash, sender key, encryption status, and size — never the message content.

3

Call /v1/verify for proof

For high-assurance flows, verify the envelope hash against the live platform to get the full integrity proof and identity verdict.

4

Act on the result

Route to Slack, trigger Shopify flows, anchor on-ledger, update a database — the envelope hash is your universal correlation key.