# Send Your First Webhook with Webhook Admin (Node.js)

> Send your first webhook with Webhook Admin in Node.js: create a consumer and endpoint, send with an Idempotency-Key, check the log and verify the signature.

Published: 2026-11-12
Source: https://webhookadmin.com/en/blog/send-first-webhook-node/

## Key points

- Five steps get your first webhook delivered: sign up, create a project, create an API key, register a consumer and endpoint, send.
- With the webhookadmin npm package, the sending side - consumer, endpoint, send and delivery check - is about 20 lines of Node.js.
- The Free plan includes 50,000 messages a month and one project. Test-environment messages and endpoint test sends aren't counted.

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

1. Sign up
2. Create a project
3. Create an API key
4. Register a consumer and an endpoint
5. Send

Then check the delivery log and verify the signature where the webhook lands.

## 1. Sign up

Go to [https://app.webhookadmin.com/signup](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`.

```bash
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.

```ts
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](https://webhookadmin.com/en/blog/test-webhooks-locally/)).
- IP-literal URLs, internal hostnames, and names that resolve to internal addresses are rejected with a 422.
- Omit `event_types` to receive every event type.
- `secret` is 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:

```ts
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:

```ts
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](https://webhookadmin.com/en/docs/sdk/). 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](https://webhookadmin.com/en/#pricing).

## Where to go next

- Step-by-step docs: [sign up](https://webhookadmin.com/en/docs/#signup), [project](https://webhookadmin.com/en/docs/#project), [API key](https://webhookadmin.com/en/docs/#api-key), [consumer and endpoint](https://webhookadmin.com/en/docs/#consumer), [send](https://webhookadmin.com/en/docs/#send), [verify](https://webhookadmin.com/en/docs/#verify)
- Everything a sender needs: [how to send webhooks](https://webhookadmin.com/en/blog/how-to-send-webhooks/)
- Why the signature looks the way it does: [Standard Webhooks explained](https://webhookadmin.com/en/blog/standard-webhooks-explained/)

All of this works on the free plan. Sign up at [https://app.webhookadmin.com/signup](https://app.webhookadmin.com/signup) and send your first message with a test key.

## 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.
