# Increase Webhooks

## When to Use This Skill

* How do I receive Increase webhooks?
* How do I verify the `webhook-signature` header from Increase?
* Why does the `standardwebhooks` library fail on my Increase webhooks?
* How do I handle `transaction.created`, `account.created`, `ach_transfer.updated`, or `card_payment.created` events?
* Why is my Increase webhook signature verification failing?

## Essential Code (USE THIS)

Increase uses the Standard Webhooks header names and signed content, **but not the key format**. The Event Subscription's `shared_secret` (1-100 characters, random by default) is the HMAC key as **raw UTF-8 bytes**. It is not `whsec_`-prefixed and not base64. The `standardwebhooks` library base64-decodes the key, so it rejects valid Increase webhooks when given the secret as-is. Verify manually.

| Header | Description |
| --- | --- |
| `webhook-id` | The Event ID, e.g. `event_123abc` (use it as an idempotency key) |
| `webhook-timestamp` | Unix seconds when the webhook was sent |
| `webhook-signature` | `v1,<base64>`; several space-separated values while a secret is rotating |

Signed content is `<webhook-id>.<webhook-timestamp>.<raw body>`, HMAC-SHA256, base64, prefixed with `v1,`.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyIncrease(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const header = headers['webhook-signature'];
  if (!secret || !id || !timestamp || !header) return false;

  // secret is used as-is: raw UTF-8 bytes, no base64 decoding
  const expected = 'v1,' + crypto.createHmac('sha256', secret)
    .update(`${id}.${timestamp}.`).update(rawBody).digest('base64');

  return header.split(' ').some((sig) => {
    const a = Buffer.from(sig), b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

```

### Verification (Python)

```python
import base64, hashlib, hmac

def verify_increase(raw_body: bytes, headers, secret: str) -> bool:
    wid, ts, header = (headers.get(h) for h in ("webhook-id", "webhook-timestamp", "webhook-signature"))
    if not (secret and wid and ts and header):
        return False
    digest = hmac.new(secret.encode(), f"{wid}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(digest).decode()
    return any(hmac.compare_digest(s.encode(), expected.encode()) for s in header.split(" "))

```

### Timestamp window (opt-in)

Increase suggests rejecting timestamps more than 5 minutes old. **Hookdeck retries and replays resend the original timestamp, so a 5-minute window rejects late retries.** The examples therefore leave the check off unless `INCREASE_TIMESTAMP_TOLERANCE_SECONDS` is set to a positive number. [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/increase-webhooks/references/verification.md) shows where the window can be dropped or widened. Duplicate deliveries are handled by deduplicating on `webhook-id`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/increase-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/increase-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/increase-webhooks/examples/fastapi/).

## Common Event Types

The payload is a thin Event: `{id, category, associated_object_type, associated_object_id, created_at, type: "event"}`. Dispatch on `category`, then `GET` the object from the Increase API for its fields.

| Category | Description |
| --- | --- |
| `transaction.created` | A settled Transaction was created on an Account |
| `pending_transaction.created` | A Pending Transaction (a hold) was created |
| `account.created` | An Account was created |
| `ach_transfer.created` | An ACH Transfer was created |
| `ach_transfer.updated` | An ACH Transfer changed status (submitted, returned, ...) |
| `card_payment.created` | A Card Payment started (authorization) |
| `inbound_ach_transfer.created` | An ACH debit or credit arrived |
| `wire_transfer.updated` | A Wire Transfer changed status |

Increase has about 120 categories, all of the form `<object>.created` or `<object>.updated`, and may add more, so ignore unknown ones gracefully. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/increase-webhooks/references/overview.md) and the [Events API](https://increase.com/documentation/api/events).

## Environment Variables

```bash
INCREASE_WEBHOOK_SECRET=your_shared_secret   # the Event Subscription's shared_secret
INCREASE_TIMESTAMP_TOLERANCE_SECONDS=0       # optional; a positive value opts in to the replay window

```

If you choose your own `shared_secret`, don't start it with `whsec_`: Standard Webhooks tooling treats a `whsec_` secret as base64, while Hookdeck's Increase source uses any secret that doesn't start with `whsec_` as UTF-8 bytes. Using the secret Increase generates, or one without that prefix, avoids the mismatch.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 increase --path /webhooks/increase

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/increase-webhooks/references/overview.md) - Increase webhook concepts, retries, and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/increase-webhooks/references/setup.md) - Creating an Event Subscription
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/increase-webhooks/references/verification.md) - Signature verification, secret handling, timestamp window