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
- Webhooks — the push path; same shapes.
- Webhook endpoints — register your destinations.
- API → Events — full event-type catalog.