Events

The events API is the persisted log Plugipay delivers webhooks from. Every state-change in the workspace — checkout_session.completed, invoice.paid, refund.succeeded, subscription.canceled, etc. — is appended here. You can read it directly for backfill (catching up on missed webhook deliveries), audit (proving what your workspace saw at a point in time), or for systems that prefer pull over push. The Go SDK exposes the two event methods behind client.Events. For the full event-type catalog and per-type payload shapes, see API → Events.

Field on the Client

client.Events — type *plugipay.EventsResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.

Methods

List

Signature. func (r *EventsResource) List(ctx context.Context, params EventListParams) (Page[EventRecord], error)

Cursor-paginated stream of events. Filters: Type (e.g. "invoice.paid"), OccurredAfter / OccurredBefore (ISO timestamps), Order ("asc" or "desc" on OccurredAt). For backfill, walk with Order: "asc" from your last known OccurredAt.

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

after := "2026-05-12T00:00:00Z"
order := "asc"
limit := 500

page, err := client.Events.List(ctx, plugipay.EventListParams{
    OccurredAfter: &after,
    Order:         &order,
    Limit:         &limit,
})
if err != nil { return err }
for _, ev := range page.Data {
    log.Printf("%s %s id=%s", ev.OccurredAt, ev.Type, ev.ID)
}

Filter to one type:

typ := "checkout_session.completed"
page, err := client.Events.List(ctx, plugipay.EventListParams{Type: &typ})

Get

Signature. func (r *EventsResource) Get(ctx context.Context, id string) (*EventRecord, error)

Fetches one event by id. Useful for confirming a webhook you received — if you have its id, you can re-pull it from the source of truth. Naturally idempotent.

ev, err := client.Events.Get(ctx, "evt_01HX...")
if err != nil {
    return err
}
log.Printf("type=%s occurredAt=%s", ev.Type, ev.OccurredAt)

// `ev.Data` is json.RawMessage — decode into the right concrete type:
type completedEvent struct {
    Object plugipay.CheckoutSession `json:"object"`
}
if ev.Type == "checkout_session.completed" {
    var c completedEvent
    if err := json.Unmarshal(ev.Data, &c); err != nil { return err }
    log.Printf("paid %d %s", c.Object.Amount, c.Object.Currency)
}

Types

type EventRecord struct {
    ID         string          `json:"id"`         // "evt_..."
    Type       string          `json:"type"`       // "invoice.paid", etc.
    AccountID  string          `json:"accountId"`
    OccurredAt string          `json:"occurredAt"` // ISO-8601
    Data       json.RawMessage `json:"data"`       // type-discriminated by Type
}

Data is intentionally opaque (json.RawMessage) — the SDK can't statically know which concrete type to decode into without conditional generics. The convention matches the WebhookEvent shape returned by plugipay.VerifyWebhook: inspect Type, then decode Data (or Data.Object on a verified webhook) into the matching resource struct (CheckoutSession, Invoice, Subscription, ...).

Event type catalog: API → Events.

Common patterns

Backfill missed webhooks

Webhook deliveries can fail (your endpoint was down, a deploy ate them, your queue dropped). The events API is your reconciliation tool. Track the highest OccurredAt you've processed; on each backfill run, pull everything strictly after it:

func backfill(ctx context.Context, c *plugipay.Client, after string, handle func(plugipay.EventRecord) error) (string, error) {
    asc := "asc"
    limit := 500
    var cursor *string
    last := after
    for {
        page, err := c.Events.List(ctx, plugipay.EventListParams{
            OccurredAfter: &after,
            Order:         &asc,
            Limit:         &limit,
            Cursor:        cursor,
        })
        if err != nil { return last, err }
        for _, ev := range page.Data {
            if err := handle(ev); err != nil { return last, err }
            last = ev.OccurredAt
        }
        if !page.HasMore { return last, nil }
        cursor = page.Cursor
    }
}

Persist last in your own DB (atomic upsert keyed on (workspace_id, "events_cursor")). Next run starts from there. The events API is idempotent — reprocessing the same evt_* id is safe as long as your handler is too.

Decoding by type

A discriminator helper:

type CheckoutCompleted struct {
    Object plugipay.CheckoutSession `json:"object"`
}
type InvoicePaid struct {
    Object plugipay.Invoice `json:"object"`
}

func decode(ev plugipay.EventRecord) (any, error) {
    switch ev.Type {
    case "checkout_session.completed":
        var v CheckoutCompleted
        return v, json.Unmarshal(ev.Data, &v)
    case "invoice.paid":
        var v InvoicePaid
        return v, json.Unmarshal(ev.Data, &v)
    }
    return nil, fmt.Errorf("unhandled event type: %s", ev.Type)
}

For inbound webhooks (push path), use plugipay.VerifyWebhook instead — it does signature verification and returns the same Data.Object shape. See Webhooks.

Pull-only architectures

If your environment can't expose a public webhook endpoint (firewall, on-prem, batch ETL only), a simple cron is enough:

// Run every minute. Persist `cursor` between runs.
cursor, err := loadCursor()
if err != nil { return err }

newCursor, err := backfill(ctx, c, cursor, func(ev plugipay.EventRecord) error {
    return processOne(ev)
})
if err != nil { return err }

if newCursor != cursor {
    return saveCursor(newCursor)
}

Context cancellation

ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second)
defer cancel()
page, err := c.Events.List(ctx, plugipay.EventListParams{Limit: &limit})

If a long backfill exceeds the deadline, the SDK returns *plugipay.Error{Code: "canceled"} — checkpoint the last processed OccurredAt and resume from there next run.

Errors

The events API is read-only:

Code Status Cause
validation_error 400 Bad OccurredAfter / OccurredBefore format, unknown event Type.
not_found 404 Event id doesn't exist.
rate_limited 429 Too many calls. Back off.
insufficient_scope 403 Key lacks plugipay:event:read.
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "rate_limited" {
    time.Sleep(2 * time.Second) // standard backoff
}

Full mechanics: Errors.

Next

Plugipay — Payments that don't tax your success