Guide to Increase Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Event Subscriptions in the dashboard or the Event Subscriptions API |
| Environments | Live and sandbox subscriptions are separate, and each receives only its own environment's Events |
| Event selection | Optional selected_. Leave it empty to receive every category |
| Signature | webhook-signature: v1,<base64> (HMAC-SHA256), several values separated by spaces during secret rotation |
| Signed content | <webhook-id>.<webhook-timestamp>.<raw body> |
| Secret | shared_secret, 1 to 100 characters, random unless you set one. Used as raw UTF-8 key bytes |
| Payload | Thin Event: id, created_at, category, associated_object_type, associated_object_id, type |
| Deduplication key | webhook-id, which is the Event ID |
| Timeout | A 2xx within 10 seconds |
| Retries | Production: up to 8 attempts over about 72 hours, for any non-2xx response. Sandbox: none |
| Event history | List Events API, for 30 days |
| IP addresses | 34.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.
| Category | Sent when |
|---|---|
transaction.created | A settled Transaction posts to an Account |
pending_ | A Pending Transaction (a hold) is created |
account.created | An Account is created |
ach_transfer.updated | An ACH Transfer changes status, for example when it is submitted or returned |
inbound_ | An inbound ACH debit or credit arrives |
card_payment.created | A Card Payment starts with an authorisation |
wire_transfer.updated | A Wire Transfer changes status |
real_time_decision.* | Increase asks your system for a decision, for example real_ |
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_.
Setting up Increase webhooks
Create an Event Subscription in the Increase dashboard at dashboard.increase.com/developers/webhooks, or with POST /event_ and a body such as:
{ "url": "https://example.com/webhooks/increase" }
Add selected_ 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
2xxwithin 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.