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-Signatureheader? - Which key signs Proof webhooks? (the API key that created the subscription, or its
signing_key) - Why is there no
X-Notarize-Signatureheader on my webhooks? (created with OAuth, nosigning_key) - How do I handle
transaction.completed,transaction.releasedornotary.createdevents?
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_ 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(). NeverJSON.parse/JSON.stringifybefore verifying. - Only
200acknowledges. 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
eventfield.dataholds IDs anddate_occurred; fetch the full transaction from the Proof API usingdata.transaction_id.
Common Event Types
| Event | Triggered When |
|---|---|
transaction.created | A transaction is created |
transaction.completed | A transaction reaches the completed state |
transaction.released | Documents are finalized and downloadable (prefer this over completed) |
transaction.declined | A transaction is declined |
transaction.held_ | A transaction is held for review |
notary.created | A 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
- references/overview.md - Payloads, all event types, delivery and retries
- references/setup.md - Creating a subscription, API key vs OAuth,
signing_key - references/verification.md - Signature details, gotchas, debugging