Events
Events are a queryable archive of every state change in your Plugipay workspace. The same payment.succeeded, refund.created, subscription.updated notifications that fire on your webhook endpoints are also written to this archive — pull them at any time with the standard HMAC-signed API.
If webhooks are the push channel, events are the pull channel. Same payload shape, same id, same type.
This page covers querying events. For the full catalog of event types (what fires when), how delivery and retries work, and how to verify webhook signatures, see Webhooks.
When to use this resource
Three common reasons to reach for events:
- Backfill missed webhooks. Your endpoint was down and Plugipay gave up retrying after 72 hours. Pull the time window and replay locally.
- Audit history. Reconstruct what happened to a payment, subscription, or customer over time — for support, dispute response, or compliance.
- Sync state at startup. A worker boots fresh; page through events
since=<last_processed_at>to catch up before going live.
For real-time delivery, prefer webhooks. Polling this endpoint as a primary integration burns rate limit.
Endpoints
| Method | Path | What | Scope |
|---|---|---|---|
GET |
/v1/events/:id |
Retrieve by ID | plugipay:event:read |
GET |
/v1/events |
List with filters | plugipay:event:read |
POST |
/v1/events/replay |
Re-emit to webhook endpoints | plugipay:event:write |
POST |
/v1/events/trigger |
Synthetic event (test mode only) | plugipay:event:trigger |
Retrieve an event
GET /v1/events/:id
Returns the event with the given ID. Events are scoped to your workspace — you can't read another workspace's events.
Response
200 OK with the event object in data.
Errors
| Status | Code | When |
|---|---|---|
404 |
not_found |
Event doesn't exist or belongs to a different workspace. |
Samples
// Node
const event = await plugipay.events.retrieve('evt_01HX...');
# Python
event = plugipay.events.retrieve("evt_01HX...")
// Go
ev, err := client.Events.Retrieve(ctx, "evt_01HX...")
# curl — plugipay_curl() defined in /docs/api/authentication
plugipay_curl GET '/v1/events/evt_01HXxxxxxxxxxxxxxxxxxxxxxx'
List events
GET /v1/events
Returns a paginated list of events for the workspace, most recent first.
Query parameters
| Name | Type | Notes |
|---|---|---|
type |
string | Exact match on event type, e.g. payment.succeeded. Omit for all types. |
occurredAfter |
ISO 8601 or epoch | Only events with occurredAt > this. Synonyms: since. |
occurredBefore |
ISO 8601 or epoch | Only events with occurredAt < this. Synonyms: until. |
objectId |
string | Only events whose data.id (or data.aggregateId) matches. Useful for "all events for this payment". |
workspaceId |
string | Platform-admin keys only — constrain to a single merchant. Ignored for regular keys (always scoped to the key's workspace). |
limit |
int | Page size, 1–100. Default 20. |
cursor |
string | Pagination cursor from the previous response. See Pagination. |
order |
asc | desc |
Default desc (most recent first). |
Test-mode keys see both test and live events from their workspace; live-mode keys see only live events. (The mode is tagged on each event when it's emitted.)
Response
200 OK with an array of event objects in data and a standard cursor in meta.page. See Pagination for the envelope.
Samples
// Node — auto-pagination
for await (const evt of plugipay.events.list({
type: 'payment.succeeded',
occurredAfter: '2026-05-12T00:00:00Z',
})) {
await handle(evt);
}
# Python — manual cursor loop
cursor = None
while True:
page = plugipay.events.list(
type="payment.succeeded",
occurred_after="2026-05-12T00:00:00Z",
cursor=cursor,
limit=100,
)
for evt in page.data:
handle(evt)
if not page.has_more:
break
cursor = page.next_cursor
// Go
iter := client.Events.List(ctx, &plugipay.EventListParams{
Type: plugipay.String("payment.succeeded"),
OccurredAfter: plugipay.String("2026-05-12T00:00:00Z"),
})
for iter.Next() { handle(iter.Event()) }
# curl
plugipay_curl GET '/v1/events?type=payment.succeeded&occurredAfter=2026-05-12T00:00:00Z'
Replay events
POST /v1/events/replay
Re-emits one or more events to all subscribed webhook endpoints. Use this when your service was down and you want Plugipay to push the events again rather than polling.
Each replayed event is a new event with its own id (and metadata.replayOf pointing at the original). If your handler dedupes on event.id, the replay will look new — which is what you want, since the original was missed.
Idempotency-Key is required — see Idempotency.
Body
| Field | Type | Notes |
|---|---|---|
eventIds |
string[] | 1–10,000 event IDs to replay. Each must belong to your workspace. |
Response
201 Created:
{
"data": {
"count": 3,
"eventIds": ["evt_01HX...", "evt_01HX...", "evt_01HX..."]
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
The returned eventIds are the new event IDs — the ones that will be sent to your webhook endpoints.
Errors
| Status | Code | When |
|---|---|---|
400 |
validation_error |
Empty array, more than 10,000 IDs, or malformed IDs. |
404 |
not_found |
One or more event IDs don't exist in your workspace (whole request fails). |
Trigger a synthetic event (test mode only)
POST /v1/events/trigger
Creates a synthetic event for local development — exercise your webhook handlers without going through a full checkout/refund flow. Live-mode keys get 403 forbidden_in_live_mode. Idempotency-Key is required.
Body
| Field | Type | Notes |
|---|---|---|
type |
string | Any event type, e.g. payment.succeeded. Not validated against the canonical catalog. |
aggregateId |
string | ID of the object the event is about. Stored under data.aggregateId. |
data |
object | Arbitrary payload, merged with aggregateId. Defaults to {}. |
201 Created returns the event object and dispatches it to your test-mode webhook endpoints normally.
The event object
{
"id": "evt_01HXxxxxxxxxxxxxxxxxxxxxxx",
"type": "payment.succeeded",
"occurredAt": "2026-05-12T10:42:00.123Z",
"createdAt": "2026-05-12T10:42:00.150Z",
"publishedAt": "2026-05-12T10:42:00.480Z",
"mode": "live",
"data": { "id": "pay_01HX...", "amount": 250000, "currency": "IDR", "status": "succeeded", "...": "..." },
"metadata": { "source": "ingress.xendit", "requestId": "req_01H..." }
}
| Field | Type | Notes |
|---|---|---|
id |
string | Stable event ID, evt_<ulid>. Same ID you receive in the webhook delivery. |
type |
string | <resource>.<action> — see the event catalog. |
occurredAt |
ISO 8601 | When the underlying event happened. Use this for ordering. |
createdAt |
ISO 8601 | When the event row was written to the outbox. |
publishedAt |
ISO 8601 | null | When delivery to webhook endpoints was first attempted. null if not yet published or no subscriber exists. |
mode |
live | test | null |
Which environment produced the event. null for legacy rows without mode tagging. |
data |
object | The full resource snapshot, same shape as the API returns for that resource. |
metadata |
object | Plugipay-internal context: emitter source, originating request ID, replay markers, etc. Not your metadata field on the underlying resource — that lives inside data. |
The data shape matches the webhook payload exactly — the same parser works for both.
Pagination notes
Busy workspaces accumulate events fast — thousands per hour is normal. Three things to keep in mind:
- Always page with the cursor. Offsets would skip or duplicate when new events arrive between pages.
- Pull narrow time windows. Prefer
occurredAfter+occurredBeforeover re-reading the world from the start. - Order is
descby default. For chronological backfill (process oldest first), passorder=asc.
Retention
Events are retained for at least 90 days from createdAt. In practice most workspaces have far longer history — we keep events as long as it's economical and prune the very oldest. Don't rely on events older than 90 days being present.
For durable long-term archival (tax, audit, analytics), sync events into your own data warehouse on an ongoing basis. The list endpoint ordered by createdAt is the canonical extraction path.
90 days is a floor, but not a contract. If you need a 7-year audit trail for compliance, your warehouse owns that — not us.
Rate limits
List and retrieve are in the read class; replay is in mutating_heavy (replays produce real webhook deliveries). See Rate limits.
Next
- Webhooks — event type catalog, signature scheme, retry policy.
- Webhook endpoints — manage endpoints programmatically.
- Pagination — cursor mechanics for the list endpoint.
- Idempotency — required on
replayandtrigger.