# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/proof-webhooks/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.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/proof-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/proof-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/proof-webhooks/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

| 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_for_review` | A transaction is held for review |
| `notary.created` | A notary is created in your organization |

37 documented events in all: see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/proof-webhooks/references/overview.md).

## Environment Variables

```bash
# 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

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/proof-webhooks/references/overview.md) - Payloads, all event types, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/proof-webhooks/references/setup.md) - Creating a subscription, API key vs OAuth, `signing_key`
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/proof-webhooks/references/verification.md) - Signature details, gotchas, debugging