Events API
The Events API is the same event catalog as webhooks, but pulled instead of pushed — useful for an initial backfill, catching up after downtime, reconciling what your webhook receiver saw, or an integration that prefers polling over running a public HTTPS endpoint.
Catching up
Section titled “Catching up”curl -s "https://api.getonebooks.com/events?after=$LAST_SEEN_EVENT_ID&limit=100" \ -H "Authorization: Bearer $ACCESS_TOKEN"{ "data": [ { "id": "evt_…", "type": "invoice.paid", "apiVersion": "2026-09-22", "createdAt": "2026-09-22T10:03:11.000Z", "resourceType": "INVOICE", "resourceId": "cm1invoice0000000000000001", "data": { "…": "…" } } ], "hasMore": true, "nextCursor": "evt_…"}| Parameter | Meaning |
|---|---|
after | Return events after this event id (exclusive) — pass the last id you processed. An id from another business, or one that has aged out of the 30-day retention window, is rejected with 400 UNKNOWN_CURSOR. |
since | An ISO 8601 timestamp: only events recorded at or after it. Can be combined with after. |
types | Comma-separated event names to filter to, e.g. invoice.paid,payment.received. An unknown name is rejected with 400 UNKNOWN_EVENT_TYPE_TYPES. |
limit | Page size — default 50, 1–100 |
Events come oldest first. Keep paging with nextCursor while hasMore is
true. When you’ve caught up, hasMore is false and nextCursor is
null — so store the id of the last event you processed yourself, and
pass it as after on your next poll. The
Node SDK’s client.events.iterate({ after })
does the paging for you.
Each event carries resourceType and resourceId — the record it’s about —
so you can correlate events with records without parsing data. The two are
always set together or both null:
| Events | resourceType / resourceId |
|---|---|
| Business events | The record the event is about: INVOICE and the invoice id, PAYMENT and the payment id, JOURNAL_ENTRY and the entry id for journal_entry.created, … |
customer.redact, supplier.redact | CUSTOMER or SUPPLIER and the erased contact’s id |
app_data.updated | The record the edited field is on (the invoice, the customer, …), not the app data value |
app.installed, app.uninstalled, app.scopes_updated, business.redact | null — they’re about your installation, not a record |
Fetch a single event by id with GET /events/:id (404 EVENT_NOT_FOUND if it
doesn’t exist or isn’t visible to you).
The 2-second window
Section titled “The 2-second window”GET /events only lists events recorded at least 2 seconds ago. A newer
event isn’t lost: it appears on your next poll with the same after. The
short hold lets every change recorded around the same moment finish being
saved first, so an event can never land behind a cursor you’ve already moved
past. hasMore: false therefore means you’re caught up to about 2 seconds
ago. GET /events/:id isn’t held back — it returns an event as soon as it’s
recorded.
Who sees what
Section titled “Who sees what”This endpoint is OAuth-bearer only: a request with a OneBooks session
cookie gets 403 EVENT_LOG_AVAILABLE_CONNECTED_APPS_ONLY, since there’s no
calling app to scope visibility to. It requires the events:read scope, and
you only ever see events for the one business your token is bound to.
Which events, specifically:
- Business and privacy events — visible when your app’s
ACTIVEinstallation in that business holds the event’s gating scope, exactly the rule webhooks use. - App-targeted events (
app.installed,app.uninstalled,app.scopes_updated,business.redact,app_data.updated) — visible whenever they’re addressed to your app, regardless of installation state. Anapp.uninstalledfor your own app is always retrievable here, even though by definition there’s no active installation left to hold a scope.
Retention
Section titled “Retention”Events are retained for 30 days, and so is webhook delivery history: after that an event can be read neither from this API nor by redelivering a webhook, and a cursor pointing at it is rejected. Poll or process within that window.
If a customer or supplier is erased, the payloads of the earlier
customer.created/customer.updated (or supplier.created/supplier.updated)
events about them — the ones that carry a name and email — are reduced to
{ "id": …, "redacted": true }. The events stay in the sequence, but the
person’s details are gone; events that mention the contact only by id (an
invoice’s customerId, say) are unchanged. See
Privacy & data handling.
Delivery semantics
Section titled “Delivery semantics”- Events are recorded after the fact. OneBooks records an event right after the business change it describes has been committed to the books. Recording is best-effort: in rare failures (a crash between the two, say) a change can be committed without its event. The REST API is the source of truth — for anything that matters, reconcile against it (for example, list the invoices changed since your last sync) rather than assuming the event stream is complete.
- Delivery is at-least-once. A crash mid-processing, a retried poll, or
overlapping catch-up runs can hand you the same event twice —
deduplicate on
id, the same discipline webhook handlers need. - Order is approximate. Order across different event types is not
guaranteed; within a single resource, order by
createdAt, not by the order deliveries arrive.
Errors
Section titled “Errors”| HTTP | code | Cause |
|---|---|---|
| 400 | UNKNOWN_CURSOR | after isn’t an event id in your token’s business (or has aged out) |
| 400 | UNKNOWN_EVENT_TYPE_TYPES | types names an event that isn’t in the catalog |
| 403 | EVENT_LOG_AVAILABLE_CONNECTED_APPS_ONLY | Called with a session cookie instead of an OAuth token |
| 403 | — | The token lacks events:read (Insufficient scope. Required: …) |
| 404 | EVENT_NOT_FOUND | GET /events/:id for an event that doesn’t exist or isn’t visible to your app |
Where next
Section titled “Where next”Hosted functions run your code automatically on these same events — no polling loop required at all.