Gareth Wilson Gareth Wilson

Guide to Increase Webhooks: Features and Best Practices

Published


Increase is a banking API: accounts, ACH, wires, checks, Real-Time Payments and cards, built for companies that move money in their own products. Its webhooks tell you when something changes on one of those objects, such as a transaction posting, an ACH transfer being returned or a card being authorised, so your ledger and customer notifications keep up without polling.

This guide covers how Event Subscriptions work, how to verify the signature and why Standard Webhooks libraries reject it, the thin Event payload, Increase's retry schedule, and the best practices for running a receiver in production.

What are Increase webhooks?

When an object in Increase is created or changes, Increase records an Event. Each Event Subscription that matches it receives an HTTPS POST with a small JSON body describing the Event. The body names the object that changed but does not include its fields, so you fetch the object from the API to see what happened.

Increase signs every delivery using the Standard Webhooks header names and signed content. The key is different from what Standard Webhooks tooling expects: the subscription's shared_secret is used as raw text, not as a base64 whsec_ key.

Increase webhook features

FeatureDetails
ConfigurationEvent Subscriptions in the dashboard or the Event Subscriptions API
EnvironmentsLive and sandbox subscriptions are separate, and each receives only its own environment's Events
Event selectionOptional selected_event_categories. Leave it empty to receive every category
Signaturewebhook-signature: v1,<base64> (HMAC-SHA256), several values separated by spaces during secret rotation
Signed content<webhook-id>.<webhook-timestamp>.<raw body>
Secretshared_secret, 1 to 100 characters, random unless you set one. Used as raw UTF-8 key bytes
PayloadThin Event: id, created_at, category, associated_object_type, associated_object_id, type
Deduplication keywebhook-id, which is the Event ID
TimeoutA 2xx within 10 seconds
RetriesProduction: up to 8 attempts over about 72 hours, for any non-2xx response. Sandbox: none
Event historyList Events API, for 30 days
IP addresses34.83.67.223, 35.247.122.129, 34.168.16.194

Common events

The event name is the category field. Categories follow the pattern <object>.created or <object>.updated, and there are more than 120 of them.

CategorySent when
transaction.createdA settled Transaction posts to an Account
pending_transaction.createdA Pending Transaction (a hold) is created
account.createdAn Account is created
ach_transfer.updatedAn ACH Transfer changes status, for example when it is submitted or returned
inbound_ach_transfer.createdAn inbound ACH debit or credit arrives
card_payment.createdA Card Payment starts with an authorisation
wire_transfer.updatedA Wire Transfer changes status
real_time_decision.*Increase asks your system for a decision, for example real_time_decision.card_authorization_requested

Every delivery has the same shape. Here is Increase's transaction.created example:

{
  "id": "event_123abc",
  "created_at": "2020-01-31T23:59:59Z",
  "category": "transaction.created",
  "associated_object_type": "transaction",
  "associated_object_id": "transaction_abc123",
  "type": "event"
}

To get the amount, description and account of that transaction, call GET /transactions/transaction_abc123.

Setting up Increase webhooks

Create an Event Subscription in the Increase dashboard at dashboard.increase.com/developers/webhooks, or with POST /event_subscriptions and a body such as:

{ "url": "https://example.com/webhooks/increase" }

Add selected_event_categories to limit what the subscription receives. A Real-Time Decision category has to be the only category on its subscription, so give those their own subscription.

Leave shared_secret out to have Increase generate one, then store the value from the Event Subscription as INCREASE_WEBHOOK_SECRET. If you choose your own, do not start it with whsec_. Create a separate subscription, with its own secret, for sandbox and for production.

If you receive Increase webhooks through Hookdeck, paste the secret Increase generated for the subscription into the Hookdeck source exactly as shown. Hookdeck uses any secret that does not start with whsec_ as raw UTF-8 bytes, which matches how Increase signs. A secret beginning with whsec_ is decoded as base64 instead, and verification fails.

Securing Increase webhooks

Build <webhook-id>.<webhook-timestamp>.<raw body>, compute an HMAC-SHA256 with the secret as raw bytes, base64-encode it and prefix v1,. Compare the result in constant time against each space-separated value in webhook-signature, and accept the request if any one matches.

Increase's documentation suggests the Standard Webhooks libraries. Those libraries base64-decode the secret before signing, so they reject valid Increase deliveries when given the shared_secret as it is. Verify manually:

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

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;

  // The shared 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');

  const expected_buffer = Buffer.from(expected);
  return header.split(' ').some((signature) => {
    const received = Buffer.from(signature);
    return received.length === expected_buffer.length && crypto.timingSafeEqual(received, expected_buffer);
  });
}

const app = express();

app.post('/webhooks/increase', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyIncrease(req.body, req.headers, process.env.INCREASE_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id, route on event.category, then fetch the object asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


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

Increase also suggests rejecting a webhook-timestamp more than a few minutes old to limit replays. That check is optional. Hookdeck retries and replays send the original timestamp, so a five-minute window would reject a late retry. If you add one, make it generous and rely on deduplicating by webhook-id for repeats.

Make Increase webhooks production-ready. Hookdeck Event Gateway verifies the Increase signature, stores every Event, and retries delivery to your services on your own schedule.

Increase webhook limitations and pain points

Thin events mean a second API call

The Problem: A webhook says a transaction was created but not how much it was for or which account it hit.

Why It Happens: Increase sends only the Event's identifiers. Every handler that needs details has to call the API, which adds latency, rate-limit pressure and another thing that can fail.

Workarounds:

  • Acknowledge first, then fetch the object in a background job.
  • Treat the fetched object as the current state, since it may have changed again since the Event.

How Hookdeck Can Help: Hookdeck queues Events and can limit the delivery rate to your service, so a burst of transactions does not turn into a burst of API calls. See handling thin events with Hookdeck.

The secret format breaks standard libraries

The Problem: Signature checks fail on every request even though the secret is correct.

Why It Happens: Increase uses the Standard Webhooks headers and signed content but treats shared_secret as raw text. Standard Webhooks libraries expect a base64 key, usually prefixed whsec_, and decode it first.

Workarounds:

  • Verify manually, as shown above.
  • Do not add whsec_ to the secret or choose a secret that starts with it.

How Hookdeck Can Help: Hookdeck's Increase source uses the secret as Increase does and rejects requests that fail with a VERIFICATION_FAILED cause, so they never become events for your services.

No retries in sandbox, and none for long outages

The Problem: Events go missing during testing, or after an outage longer than three days in production.

Why It Happens: Sandbox deliveries are attempted once. In production, the last of 8 attempts is about 72 hours after the Event. Any non-2xx response or a response slower than 10 seconds counts as a failure, 4xx included.

Workarounds:

  • Respond with 2xx within 10 seconds and do the work later.
  • Poll the List Events API as a backstop, as Increase recommends. Events stay there for 30 days.

How Hookdeck Can Help: Hookdeck accepts and stores each delivery as it arrives, then delivers to your service with automatic retries and manual replay, and raises issues when deliveries keep failing.

Best practices

Deduplicate on webhook-id

webhook-id is the Event ID, the same value as id in the body, and it stays the same on every retry. Record processed IDs and skip repeats. See our guide to webhook idempotency.

Route on category and ignore unknown ones

Switch on category and acknowledge categories you do not handle with 200. Increase has many categories and can add more.

Fetch the object, do not infer from the category

Use associated_object_type and associated_object_id to GET the object, and act on its current status. By the time you process an ach_transfer.updated Event, the transfer may have changed again.

Keep Real-Time Decisions separate

Increase requires a Real-Time Decision category to be the only category on its subscription. Give each one its own subscription and keep its handler separate from routine Events.

Acknowledge fast and process asynchronously

Return 200 once the signature checks out and the Event is stored, then fetch and process. See why to process webhooks asynchronously.

Allowlist Increase's IP addresses

Increase sends webhooks from 34.83.67.223, 35.247.122.129 and 34.168.16.194. Restrict the endpoint to those addresses as well as checking the signature.

Conclusion

Increase sends thin Events to each matching Event Subscription, signed with Standard Webhooks headers but keyed with a raw shared secret. Verify the signature manually, deduplicate on webhook-id, route on category, fetch the object for details, and answer within 10 seconds, because sandbox never retries and production stops after about three days.

Hookdeck Event Gateway verifies Increase signatures, stores every Event, and redelivers on your terms, so a busy period or an outage on your side does not leave gaps in your ledger.

Get started with Hookdeck for free and handle Increase webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.