Test Modern Treasury webhooks locally
Receive Modern Treasury webhook events on localhost with a Horizon tunnel, and verify the X-Signature header with the official Node SDK.
Receive Modern Treasury events on your laptop while you build, with a public HTTPS URL Modern Treasury can reach.
Horizon has no Modern Treasury integration. Modern Treasury sends webhooks to a public URL, and Horizon provides that URL.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A Modern Treasury organization where you can open Developers
- A Next.js app that uses the App Router and runs on port 3000
Start your app
Modern Treasury signs every payload with HMAC-SHA-256 (a keyed hash) of the raw body, keyed with your webhook key. The signature is hex encoded and arrives in the X-Signature header. The official modern-treasury Node SDK has webhooks.validateSignature to check it. Install it:
npm install modern-treasuryCreate the route handler. Read the raw body with request.text() and don't parse or change it before you validate. The SDK client also needs your API key and organization ID, so you set three environment variables.
import ModernTreasury from "modern-treasury";
export async function POST(request: Request) {
const client = new ModernTreasury();
const body = await request.text();
let isValid = false;
try {
isValid = client.webhooks.validateSignature(body, request.headers);
} catch {
isValid = false;
}
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
const topic = request.headers.get("x-topic");
const webhookId = request.headers.get("x-webhook-id");
console.log(`Received Modern Treasury webhook: ${topic} (${webhookId})`);
return new Response("ok", { status: 200 });
}new ModernTreasury() reads its settings from the environment. validateSignature throws when the X-Signature header is missing, so the handler catches the error and rejects the request.
Modern Treasury sends each webhook with a unique X-Webhook-ID header. The ID stays the same when Modern Treasury sends a webhook again, so you can use it to process each webhook once.
Add your credentials to .env.local. You add the webhook key in a later step.
MODERN_TREASURY_API_KEY=replace-with-your-api-key
MODERN_TREASURY_ORGANIZATION_ID=replace-with-your-organization-idStart the app:
npm run devStart a tunnel
In a second terminal, open a tunnel to port 3000 on a subdomain you reserved, with -s:
hrzn tunnel http://localhost:3000 -s my-treasury-appHORIZON: Tunnel connected
URL https://my-treasury-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-treasury-appUse -s. Without it, the subdomain is random and changes on every run, and you would have to edit the endpoint in Modern Treasury each time you restart. Reserved subdomains are a paid feature, see Pricing.
Add the endpoint in Modern Treasury
- Open the Developers section in Modern Treasury and select the Webhooks tab.
- Select Create New Webhook Endpoint.
- Set the URL to
https://my-treasury-app.hrzn.run/api/webhooks/modern-treasury. - Choose which events the endpoint receives. If you choose individual events, Modern Treasury shows a list of every event it sends.
- Save the endpoint and copy the webhook key.
Modern Treasury shows the webhook key only when you create the endpoint. Copy it before you leave the page.
Verify the signature
Add the webhook key to .env.local:
MODERN_TREASURY_API_KEY=replace-with-your-api-key
MODERN_TREASURY_ORGANIZATION_ID=replace-with-your-organization-id
MODERN_TREASURY_WEBHOOK_KEY=replace-with-your-webhook-keyRestart npm run dev so Next.js loads the new variable.
Check it works
Create an event in your Modern Treasury account, such as a new payment order. Modern Treasury sends a webhook for it to your endpoint.
The Horizon terminal prints one line for the request:
POST 200 /api/webhooks/modern-treasuryYour app terminal prints one line such as Received Modern Treasury webhook: payment_order (<webhook id>).
To see the delivery on Modern Treasury's side, open Developers, then Webhooks, and select your endpoint. Its overview page lists the delivery attempts. Select View Details on one to see its headers, body and destination URL.
Troubleshooting
The signature doesn't match
The Horizon line shows [401]. Check these in order:
- The key.
MODERN_TREASURY_WEBHOOK_KEYmust equal the webhook key of this endpoint. Each endpoint has its own key. Did you restart the dev server after you edited.env.local? - The body. Validate the raw body. Don't call
request.json()first. - The header. Modern Treasury sends the signature in
X-Signature.
The handler throws about a missing API key or organization ID
new ModernTreasury() needs MODERN_TREASURY_API_KEY and MODERN_TREASURY_ORGANIZATION_ID as well as the webhook key. Set all three in .env.local.
The request returns 404
The Horizon line shows [404]. The route file app/api/webhooks/modern-treasury/route.ts serves /api/webhooks/modern-treasury. Check the endpoint URL for typos, and make sure the file exports POST.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Modern Treasury still sends events to the old URL. Restart with -s, and edit the endpoint if the URL differs.
Next steps
- Read Modern Treasury's guide to verifying a webhook event.
- Reserve a subdomain so your URL never changes: see pricing.