Webhook endpoints
A webhook endpoint is a URL Plugipay POSTs to when something happens in your workspace — checkout completed, invoice paid, subscription canceled. You register the URL once, store the returned signing secret, and Plugipay starts pushing events to it. This page covers the plugipay.webhookEndpoints namespace; for verifying the incoming events themselves, see Webhooks, and for the HTTP-level surface see API: Webhook endpoints.
Namespace
plugipay.webhookEndpoints — every method on this namespace:
plugipay.webhookEndpoints.list()
plugipay.webhookEndpoints.create(input)
plugipay.webhookEndpoints.delete(id)
There's no update — if you need to change the URL or events filter, delete and re-create. The signing secret is bound to the endpoint and rotates when you re-create.
Methods
webhookEndpoints.list
Signature. plugipay.webhookEndpoints.list(): Promise<WebhookEndpoint[]>
Returns every endpoint registered to this workspace. Unlike most list methods on the SDK, this one does not paginate — webhook endpoints are bounded to a small number per workspace (typically < 10).
import { PlugipayClient } from '@forjio/plugipay-node';
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
const endpoints = await plugipay.webhookEndpoints.list();
for (const ep of endpoints) {
console.log(`${ep.id}: ${ep.url} (${ep.active ? 'active' : 'paused'})`);
console.log(` Subscribed to: ${ep.events.join(', ')}`);
}
The secret field on each endpoint is omitted from list responses — the secret is only returned at create time, once.
webhookEndpoints.create
Signature. plugipay.webhookEndpoints.create(input): Promise<WebhookEndpoint>
Registers a new endpoint. url is required; events is an optional list to subscribe to (omit to receive every event type); description is an optional label. The SDK auto-attaches an Idempotency-Key.
This is the only call that returns the signing secret. Capture it immediately — subsequent list calls won't include it.
const endpoint = await plugipay.webhookEndpoints.create({
url: 'https://yourapp.com/webhooks/plugipay',
events: [
'plugipay.checkout_session.completed.v1',
'plugipay.invoice.paid.v1',
'plugipay.subscription.canceled.v1',
],
description: 'Production receiver',
});
console.log(endpoint.secret);
// → 'whsec_...'
// STORE THIS NOW. It will not be returned again.
The secret appears once. Plugipay shows the signing secret only in the response to
create. If you don't capture it (e.g. you log everything except this), you have to delete and re-create the endpoint — which means re-deploying the secret to your verifier.
webhookEndpoints.delete
Signature. plugipay.webhookEndpoints.delete(id): Promise<void>
Removes the endpoint. Plugipay immediately stops delivering events to it; in-flight deliveries may still arrive briefly. The endpoint's secret becomes invalid.
await plugipay.webhookEndpoints.delete('whe_01HX...');
Types
interface WebhookEndpoint {
id: string; // 'whe_...'
accountId: string;
url: string;
events: string[]; // subscribed event types
description: string | null;
active: boolean;
secret?: string; // only on create
createdAt: string;
updatedAt: string;
}
See API: Webhook endpoints for the full set of event types you can subscribe to and the delivery retry policy.
Common patterns
Bootstrap a webhook endpoint at deploy time
In a fresh workspace, mint the endpoint and stash the secret in your secret manager:
import { PlugipayClient } from '@forjio/plugipay-node';
async function bootstrapWebhook() {
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
// Idempotency: skip if we already registered one for this URL.
const existing = await plugipay.webhookEndpoints.list();
if (existing.some((e) => e.url === process.env.WEBHOOK_URL!)) {
console.log('Endpoint already registered, skipping.');
return;
}
const endpoint = await plugipay.webhookEndpoints.create({
url: process.env.WEBHOOK_URL!,
description: `${process.env.NODE_ENV} receiver`,
});
// Store endpoint.secret in your secret manager (AWS SM, Vault, etc.)
await storeSecret('PLUGIPAY_WEBHOOK_SECRET', endpoint.secret!);
console.log(`Registered endpoint ${endpoint.id}. Secret stored.`);
}
Reduce delivered traffic with a subscription filter
By default an endpoint receives every event type. For high-volume workspaces, narrow the subscription to just what you handle:
await plugipay.webhookEndpoints.create({
url: 'https://yourapp.com/webhooks/plugipay',
events: [
// Only fulfillment-relevant events:
'plugipay.checkout_session.completed.v1',
'plugipay.invoice.paid.v1',
'plugipay.invoice.payment_failed.v1',
],
});
Plugipay won't even try to deliver other event types — the filter is server-side, so your endpoint isn't burning compute on events it doesn't care about.
Rotate the signing secret
The way to rotate is delete-and-recreate, which means a brief moment where you have two valid secrets. The safe order:
async function rotateWebhookSecret() {
// 1. Mint a new endpoint at the same URL.
const next = await plugipay.webhookEndpoints.create({
url: 'https://yourapp.com/webhooks/plugipay',
events: ['plugipay.checkout_session.completed.v1' /* ... */],
description: 'Rotation 2026-11',
});
// 2. Deploy the new secret to your verifier alongside the old.
await deployVerifierWithDualSecrets({
oldSecret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
newSecret: next.secret!,
});
// 3. After 24h grace, delete the old endpoint.
// (Find the old endpoint ID via list; delete by ID.)
}
For most workspaces with a single endpoint, the simpler rotation is: schedule maintenance, delete the endpoint, recreate, redeploy — cost is a small window of events you'll have to replay from client.events.list afterwards.
List and prune stale endpoints
After a few migrations you sometimes end up with endpoints pointing at retired URLs. Audit periodically:
async function pruneEndpointsNotIn(allowedUrls: Set<string>) {
const all = await plugipay.webhookEndpoints.list();
for (const ep of all) {
if (!allowedUrls.has(ep.url)) {
console.log(`Deleting stale endpoint ${ep.id} (${ep.url})`);
await plugipay.webhookEndpoints.delete(ep.id);
}
}
}
Pair with verification on the receiver side
Endpoint creation only gets you half of the wiring. The other half is verifying the signature on inbound events — see the top-level verifyWebhook helper. The endpoint object's secret field is exactly what you pass into the verifier.
import { verifyWebhook } from '@forjio/plugipay-node';
app.post('/webhooks/plugipay', (req, res) => {
const event = verifyWebhook({
body: req.rawBody,
signature: req.header('Plugipay-Signature')!,
secret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
});
// ... handle event ...
});
Errors
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Bad URL (not HTTPS, malformed), unknown event type in events. |
conflict |
409 | Duplicate URL already registered in this workspace. |
not_found |
404 | Endpoint ID doesn't exist (on delete). |
forbidden |
403 | Key lacks plugipay:webhook:write scope. |
Next
- Webhooks — verifying and handling the events these endpoints deliver.
- Events — the replay API for the same events.
- API: Webhook endpoints — HTTP reference.