This is the shortest path from signing up to a signed webhook arriving at your endpoint. In Node.js with the webhookadmin package, the sending side is about 20 lines, and we also show the same call without the SDK.
Getting your first webhook out of Webhook Admin takes five steps: sign up, create a project, create an API key, register a consumer and an endpoint, and send. In Node.js with the webhookadmin package, the sending side - creating the consumer and endpoint, sending, and checking the delivery - is about 20 lines. This post walks through each step, shows the same call without the SDK, and ends with signature verification on the receiving side.
The steps
- Sign up
- Create a project
- Create an API key
- Register a consumer and an endpoint
- Send
Then check the delivery log and verify the signature where the webhook lands.
1. Sign up
Go to https://app.webhookadmin.com/signup and sign up with email, Google or GitHub. With email you get a sign-in link that's valid for 15 minutes. On first sign-in you create an organization (your company or team); plans, billing and members belong to the organization.
2. Project
Create one project per product that sends webhooks. Each project has two environments:
| Environment | API key | Messages |
|---|---|---|
| Live | sk_live_… |
Count toward your plan |
| Test | sk_test_… |
Not counted |
Start in the test environment, then switch to a live key when everything works.
3. API key
Create keys per environment under "API keys" in the dashboard. The key is shown once. Pick from five scopes:
| Scope | Allows |
|---|---|
messages:send |
Sending messages |
messages:retry |
Retrying deliveries |
logs:read |
Reading delivery logs, consumers and endpoints |
endpoints:write |
Creating, updating and deleting endpoints; rotating secrets |
consumers:write |
Creating consumers |
The code in this post creates consumers and endpoints and reads logs, so give the key all five. In production, a server that only sends should get messages:send alone. Keys can expire after 30 days, 90 days, a year, or never; organization owners and admins get an email 7 days before a key expires.
Put the key in an environment variable. The SDK reads WEBHOOK_ADMIN_API_KEY.
npm install webhookadmin
export WEBHOOK_ADMIN_API_KEY=sk_test_...
4-5. Consumer, endpoint, send
A consumer is the customer who receives webhooks; external_id is that customer's ID in your system. An endpoint is a URL where that consumer receives them. When you send, you pass the consumer's external_id, an event type and a payload, and the message goes to every one of that consumer's endpoints subscribed to the event type.
import { WebhookAdmin } from 'webhookadmin';
const wha = new WebhookAdmin(); // reads WEBHOOK_ADMIN_API_KEY (sk_test_…)
// Consumer: external_id is the customer's ID in your system
const consumer = await wha.consumers.create({ external_id: 'cus_1024', name: 'Example Inc.' });
// Endpoint: the secret is only returned here
const endpoint = await wha.endpoints.create({
consumer_id: consumer.id,
url: 'https://example.com/webhooks',
event_types: ['invoice.paid'],
});
console.log(endpoint.secret); // hand this whsec_… to the receiver
// Send: the same Idempotency-Key returns the same message for 24 hours
const { id } = await wha.messages.send(
{ consumer: 'cus_1024', event_type: 'invoice.paid', payload: { invoice_id: 'inv_88', amount: 128000 } },
{ idempotencyKey: 'inv_88-paid' },
);
// Check the delivery
const message = await wha.messages.get(id);
const d = message.deliveries[0];
console.log(d?.status, d?.attempts[0]?.response_status, d?.attempts[0]?.response_head);
About endpoints:
- URLs must be https on port 443 or 8443, in the test environment too. To receive on your own machine, put an ngrok or cloudflared tunnel in front of it (testing webhooks locally).
- IP-literal URLs, internal hostnames, and names that resolve to internal addresses are rejected with a 422.
- Omit
event_typesto receive every event type. -
secretis only returned on creation. Later you can see it in the dashboard, or fetch it with developer access or above.
Sending returns 202 Accepted with { id, deliveries }, where deliveries is the number of endpoints the message will go to. If no consumer has that external_id, one is created on the spot (with no endpoints, so deliveries is 0).
Why the Idempotency-Key
Your own server can call the send API twice after a network error. Sends with the same Idempotency-Key return the first message's id for 24 hours instead of creating a second message. Build the key from something that happens once in your domain, like the invoice ID plus the event type. If you omit it, the SDK generates one and reuses it on its own retries.
Without the SDK
The API is JSON over HTTP, so fetch is enough:
const res = await fetch('https://api.webhookadmin.com/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEBHOOK_ADMIN_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'inv_88-paid',
},
body: JSON.stringify({
consumer: 'cus_1024',
event_type: 'invoice.paid',
payload: { invoice_id: 'inv_88', amount: 128000 },
}),
});
console.log(res.status, await res.json()); // 202 { id: 'msg_…', deliveries: 1 }
On top of this, the SDK waits for retry-after and retries on 429, retries 5xx and network errors only for calls that are safe to repeat, adds an Idempotency-Key automatically, and types responses and errors.
Check the delivery log
Look under "Logs" in the dashboard, or call GET /v1/messages/{id}. Each endpoint's attempts show the time, status code, first 1,024 bytes of the response body, duration and any error. If an attempt failed, read the response, fix the receiver, and replay the delivery (POST /v1/deliveries/{id}/retry).
To test an endpoint without triggering a real business event, send webhook.test to just that endpoint with POST /v1/endpoints/{id}/test. Test sends don't count toward usage.
Verify on the receiving side
Every delivery carries a Standard Webhooks signature. The receiver verifies the raw body and headers with the whsec_… from step 4:
import express from 'express';
import { Webhook } from 'webhookadmin';
const wh = new Webhook(process.env.WEBHOOK_SECRET!); // whsec_…
const app = express();
// Verify against the raw body, before any JSON parsing
app.post('/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = await wh.verify(req.body, req.headers); // { type, timestamp, data }
console.log(event.type, event.data);
res.sendStatus(204);
} catch {
res.sendStatus(400);
}
});
app.listen(3000);
Receivers don't need this SDK: the official Standard Webhooks libraries (Node, Python, Go, Ruby and others) verify the same signatures. The header and signature format is in signature verification. To drop duplicates, use the webhook-id header; it stays the same across retries.
What the Free plan covers
| Item | Free plan |
|---|---|
| Live messages | 50,000 a month (at the limit, new sends are refused until the 1st, UTC) |
| Projects | 1 |
| Delivery log retention | 7 days |
| API rate | 600 requests a minute |
| Test messages, retries, test sends | Not counted |
The customer portal and static source IPs start on Starter. All plan limits are on the pricing page.
Where to go next
- Step-by-step docs: sign up, project, API key, consumer and endpoint, send, verify
- Everything a sender needs: how to send webhooks
- Why the signature looks the way it does: Standard Webhooks explained
All of this works on the free plan. Sign up at https://app.webhookadmin.com/signup and send your first message with a test key.
Quick FAQ
What does the Free plan include?
50,000 live messages a month, one project, 7 days of delivery logs and 600 API requests a minute. Test-environment messages, retries and endpoint test sends don't count toward the monthly volume. The customer portal and static source IPs start on the Starter plan.
Can I send without the SDK?
Yes. The API is plain JSON over HTTP, so POST /v1/messages from fetch or curl works. The SDK adds retries on 429 and 5xx, automatic Idempotency-Keys and types.
Do test-environment messages count toward my usage?
No. Every project has a live and a test environment. Anything sent with an sk_test_ key is a test message and doesn't count toward your monthly limit or billing.
Thanks for reading! If you try it, which step took longer than you expected? Let us know in the comments.
This post first appeared on the Webhook Admin blog.
Top comments (0)