Agent skill

Proof Webhooks Skill

Receive and verify Proof (proof.com, formerly Notarize) webhooks. Use when setting up Proof Webhooks V2 handlers, debugging X-Notarize-Signature verification, or handling events like transaction.completed, transaction.released, transaction.created and notary.created.

Install this skill

npx skills add hookdeck/webhook-skills --skill proof-webhooks


Proof (the online notarization and identity platform, formerly Notarize) sends an HTTP POST with a JSON body to every Webhooks V2 subscription when a notary or transaction changes state. Requests are signed with an HMAC-SHA256 of the raw body in the X-Notarize-Signature header.

When to Use This Skill

  • How do I receive Proof webhooks?
  • How do I verify the X-Notarize-Signature header?
  • Which key signs Proof webhooks? (the API key that created the subscription, or its signing_key)
  • Why is there no X-Notarize-Signature header on my webhooks? (created with OAuth, no signing_key)
  • How do I handle transaction.completed, transaction.released or notary.created events?

Setup

Paste the Proof API key that created the webhook, or its signing_key if you set one. Webhooks created with OAuth must set a signing_key.

Store it as PROOF_WEBHOOK_SECRET. Subscriptions are created through the Webhook API (POST /v2/webhooks); see references/setup.md.

Verification (core)

X-Notarize-Signature is the bare lowercase hex HMAC-SHA256 of the raw request body bytes, keyed with the secret's UTF-8 bytes (not hex- or base64-decoded). Nothing else is signed: no timestamp, no id, so there is no replay window to check. Proof has no SDK verifier, so verify manually, before parsing the JSON.

const crypto = require('crypto');

function verifyProofSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  // Exactly 64 hex characters, compared as one digest (no prefix, no list)
  if (!/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest(); // rawBody: Buffer
  const received = Buffer.from(signatureHeader.toLowerCase(), 'hex'); // safe: format validated
  return crypto.timingSafeEqual(expected, received);
}

Python: hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() against the lowercased header with hmac.compare_digest on bytes. Missing, empty, malformed or mismatched header: return 401.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Handler Rules

  • Raw body, any content type. Express: express.raw({ type: '*/*' }); Next.js: await request.arrayBuffer(); FastAPI: await request.body(). Never JSON.parse / JSON.stringify before verifying.
  • Only 200 acknowledges. Any other status, or no answer within 30 seconds, is retried with back-off, up to 16 attempts across 48 hours.
  • Expect repeats and out-of-order delivery. Dedupe on your own state and skip to the latest status.
  • Dispatch on the body's event field. data holds IDs and date_occurred; fetch the full transaction from the Proof API using data.transaction_id.

Common Event Types

EventTriggered When
transaction.createdA transaction is created
transaction.completedA transaction reaches the completed state
transaction.releasedDocuments are finalized and downloadable (prefer this over completed)
transaction.declinedA transaction is declined
transaction.held_for_reviewA transaction is held for review
notary.createdA notary is created in your organization

37 documented events in all: see references/overview.md.

Environment Variables

# The API key that created the subscription, or its signing_key if you set one
PROOF_WEBHOOK_SECRET=your_proof_api_key_or_signing_key

Local Development

npx hookdeck-cli listen 3000 proof --path /webhooks/proof

No account required. Use the printed URL as the url of the Proof webhook subscription.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →