Skip to content

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.

Terminal window
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_…"
}
ParameterMeaning
afterReturn 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.
sinceAn ISO 8601 timestamp: only events recorded at or after it. Can be combined with after.
typesComma-separated event names to filter to, e.g. invoice.paid,payment.received. An unknown name is rejected with 400 UNKNOWN_EVENT_TYPE_TYPES.
limitPage 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:

EventsresourceType / resourceId
Business eventsThe 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.redactCUSTOMER or SUPPLIER and the erased contact’s id
app_data.updatedThe record the edited field is on (the invoice, the customer, …), not the app data value
app.installed, app.uninstalled, app.scopes_updated, business.redactnull — 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).

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.

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 ACTIVE installation 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. An app.uninstalled for your own app is always retrievable here, even though by definition there’s no active installation left to hold a scope.

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.

  • 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.
HTTPcodeCause
400UNKNOWN_CURSORafter isn’t an event id in your token’s business (or has aged out)
400UNKNOWN_EVENT_TYPE_TYPEStypes names an event that isn’t in the catalog
403EVENT_LOG_AVAILABLE_CONNECTED_APPS_ONLYCalled with a session cookie instead of an OAuth token
403—The token lacks events:read (Insufficient scope. Required: …)
404EVENT_NOT_FOUNDGET /events/:id for an event that doesn’t exist or isn’t visible to your app

Hosted functions run your code automatically on these same events — no polling loop required at all.