Events

An event is an immutable record of something that happened in your workspace — a checkout completed, an invoice paid, a subscription canceled. Events are the same payloads Plugipay delivers via webhooks; the events log is the read-side API for the same data, suitable for replay, debugging, and "I missed a webhook, what happened in the last hour?" recoveries. This page covers the plugipay.events namespace; for the underlying HTTP surface see API: Events, and for the typed event shapes see Webhooks.

Namespace

plugipay.events — every method on this namespace:

plugipay.events.list(params?)
plugipay.events.get(id)

Events are emitted automatically; you don't create them. The live delivery channel is webhooks — use the events API for replay and forensics.

Methods

events.list

Signature. plugipay.events.list(params?): Promise<{ data: EventRecord[]; cursor: string | null; hasMore: boolean }>

Cursor-paginated list. Filters:

  • type — an event type, e.g. 'plugipay.checkout_session.completed.v1'. Exact match; no wildcards.
  • occurredAfter, occurredBefore — ISO-8601 strings.
  • order'asc' (oldest first — the right choice for replay) or 'desc' (newest first — default, the right choice for dashboards).
  • limit, cursor.
import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
});

// All completed checkout sessions since yesterday:
const since = new Date(Date.now() - 86_400_000).toISOString();
const { data } = await plugipay.events.list({
  type: 'plugipay.checkout_session.completed.v1',
  occurredAfter: since,
  order: 'asc',
  limit: 100,
});

for (const ev of data) {
  console.log(ev.id, ev.occurredAt, ev.type);
}

events.get

Signature. plugipay.events.get(id): Promise<EventRecord>

Fetch one event by its evt_* ID. Useful when a webhook receiver logs the event ID and you want to re-inspect the full payload.

const ev = await plugipay.events.get('evt_01HX...');
console.log(ev.type, ev.data);

Types

interface EventRecord {
  id: string;                  // 'evt_...'
  type: string;                // e.g. 'plugipay.invoice.paid.v1'
  accountId: string;
  occurredAt: string;          // ISO-8601
  data: unknown;               // shape depends on type — narrow via the WebhookEvent union
}

The data field is typed unknown here because it varies per type. For the typed shapes (which mirror webhook payloads exactly), see the WebhookEvent union in Webhooks or API: Events. The narrowing pattern:

import { WebhookEvent } from '@forjio/plugipay-node';

function narrow(ev: EventRecord): WebhookEvent | null {
  return ev as unknown as WebhookEvent; // shape-compatible at runtime
}

const ev = await plugipay.events.get('evt_01HX...');
const typed = narrow(ev);
if (typed?.type === 'plugipay.checkout_session.completed.v1') {
  console.log(typed.data.object.hostedUrl);
}

Common patterns

Replay missed webhooks

The most common reason to use the events API: your webhook receiver was down for an hour, and you need to catch up. Walk the events log in asc order from the last successfully-processed event:

async function replayMissed(sinceEventId: string) {
  // Get the timestamp of the last-processed event:
  const since = (await plugipay.events.get(sinceEventId)).occurredAt;

  let cursor: string | undefined;
  while (true) {
    const page = await plugipay.events.list({
      occurredAfter: since,
      order: 'asc',
      limit: 100,
      cursor,
    });
    for (const ev of page.data) {
      await myWebhookHandler(ev as unknown as WebhookEvent);
    }
    if (!page.hasMore) break;
    cursor = page.cursor!;
  }
}

Make your webhook handler idempotent. Replay implies you may re-process events you've already seen. Key your handler on event.id and deduplicate on first sight — the same advice that applies to live webhook delivery applies double for replay.

Build an event-log dashboard

Newest-first scrolling, paginated:

async function eventPage(cursor?: string) {
  return plugipay.events.list({ order: 'desc', limit: 50, cursor });
}

// In your dashboard:
const first = await eventPage();
const next = first.hasMore ? await eventPage(first.cursor!) : null;

Filter for specific event types

For a "subscription churn" report, list canceled and expired events:

async function* churnEvents(from: string, to: string) {
  for (const type of [
    'plugipay.subscription.canceled.v1',
    'plugipay.checkout_session.expired.v1',
  ]) {
    let cursor: string | undefined;
    while (true) {
      const page = await plugipay.events.list({
        type,
        occurredAfter: from,
        occurredBefore: to,
        order: 'asc',
        limit: 100,
        cursor,
      });
      for (const ev of page.data) yield ev;
      if (!page.hasMore) break;
      cursor = page.cursor!;
    }
  }
}

Diff event vs current state

A common debugging case: "the event said the subscription was active, but the current object is canceled — when did it change?" Compare the event's snapshot with a current get:

async function subscriptionDelta(eventId: string) {
  const ev = (await plugipay.events.get(eventId)) as unknown as WebhookEvent;
  if (ev.type.startsWith('plugipay.subscription.')) {
    const eventSnapshot = ev.data.object;
    const current = await plugipay.subscriptions.get(eventSnapshot.id);
    return { wasStatus: eventSnapshot.status, isStatus: current.status };
  }
}

Build a "what happened today" digest

Used for daily ops email or Slack digests:

async function todayDigest() {
  const start = new Date();
  start.setUTCHours(0, 0, 0, 0);

  const buckets: Record<string, number> = {};
  let cursor: string | undefined;
  while (true) {
    const page = await plugipay.events.list({
      occurredAfter: start.toISOString(),
      order: 'asc',
      limit: 200,
      cursor,
    });
    for (const ev of page.data) {
      buckets[ev.type] = (buckets[ev.type] ?? 0) + 1;
    }
    if (!page.hasMore) break;
    cursor = page.cursor!;
  }
  return buckets;
}

Errors

Code Status Cause
validation_error 400 Bad ISO-8601 in occurredAfter / occurredBefore, unknown type.
not_found 404 Event ID doesn't exist.
forbidden 403 Key lacks plugipay:event:read scope.

Next

Plugipay — Payments that don't tax your success