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

Plugipay — Payments that don't tax your success