Appearance
Workspace APIs
The Public Data API gives an integration read-only access to one Engagifii workspace. Use it to copy a workspace's data into a data warehouse, CRM or reporting tool and keep that copy up to date.
- Read-only. Every endpoint is a
GET. Request bodies are ignored. - One workspace per token. The token decides which workspace you read. No header or parameter can select another one.
- JSON in, JSON out. Successful responses are
application/json. Errors areapplication/problem+json.
Base URL
text
https://{api-host}/api/v1All paths on these pages are relative to the base URL. For example, GET /people means GET https://{api-host}/api/v1/people.
Pages
| Page | Resources |
|---|---|
| Workspace and token | /whoami, /workspace |
| People and organizations | /people, /organizations, /groups, /committees, /committee-members, /profile-change-requests, /files |
| Interactions | /interactions, /interactions/{id}/body, /interaction-participants, /interaction-organizations |
| Events | /events, /event-sessions, /event-bundles, /event-booths, /booth-staff, /pricing-rules |
| Registrations | /registrations, /registration-items, /registration-answers, /registration-activities |
| Training | /classes, /courses, /class-registrations, /training-credits, /course-credits |
| Billing and accounting | /invoices, /payments, /credit-notes, /refunds, /payment-batches, /invoice-reminders, /transaction-activities, /gl-accounts, /departments, /gl-assignments |
Authentication
Send the token in the Authorization header on every request:
http
GET /api/v1/whoami HTTP/1.1
Host: {api-host}
Authorization: Bearer {token}- Engagifii issues tokens for each integration. Ask your Engagifii contact for one.
- A token is valid for 365 days unless a different lifetime was agreed.
GET /whoamireturnstoken_expires_at. - Issuing a new token for an integration cancels its old token straight away.
- The token is accepted only in the
Authorizationheader, never in the query string. - A missing, wrong, expired or revoked token gets
401 Unauthorized.
Call GET /whoami first to confirm a new token works and names the workspace you expect.
Rate limit
Each integration may make 120 requests per minute. Other integrations do not count against it. Over the limit, the API returns 429 Too Many Requests with a Retry-After header that gives the number of seconds to wait.
GET /interactions/{id}/body has its own limit of 600 requests per minute, counted separately, so a first full sync can read one body per interaction.
Response shape
A list endpoint returns one page of records and a cursor for the next page:
json
{
"data": [ { "id": "3f0c…", "…": "…" } ],
"next_cursor": "WyIyMDI2LTEwLTAxVDA5OjMwOjAwLjEyMyIsIjNmMGMuLi4iXQ"
}A single-record endpoint (GET /{resource}/{id}) returns { "data": { … } }.
Conventions in every record:
- Ids are lower-case UUIDs. An id is the same wherever it appears: a
person_idon an invoice is theidfrom/people. - Times are UTC in ISO 8601 with seconds, for example
2026-12-01T13:00:00Z. - Dates without a time are
YYYY-MM-DD. - Most text fields come back as
nullwhen they have no value. A few, such as record numbers and names, can come back as"", so treat""andnullthe same. - Money is an object with the amount as a decimal string in dollars, two decimal places, for example
{ "amount": "850.00", "currency": "USD" }. Parse the string as a decimal, not a floating-point number. - Every record has
updated_at, the time it last changed. The rows of/interaction-participantsand/interaction-organizationsare the exception, and they page differently; see Interactions. - Text fields are returned as Engagifii stores them. Notes, comments, email bodies and HTML custom fields can contain HTML. It is not sanitised: never render it unescaped.
Paging
List endpoints use cursor paging, ordered by updated_at and then id.
| Parameter | Meaning |
|---|---|
page_size | Records per page, from 1. Default 100, maximum 500. /invoices and /people allow at most 200; /interaction-participants and /interaction-organizations allow up to 5000. A larger value gets 400. |
cursor | The next_cursor from the previous page, passed back unchanged. |
updated_since | Only records that changed at or after this time. ISO 8601 date or date-time, with or without an offset: 2026-10-01, 2026-10-01T09:30:00Z or 2026-10-01T09:30:00-04:00. |
Keep requesting with the latest next_cursor and the same filters until it is null. Records that change while you page are not repeated or skipped, as can happen with page-number paging.
A cursor that has been changed or cut short gets 400 with "The cursor is invalid. Use next_cursor from the previous page unchanged."
Why a cursor, not page numbers
Responses have no page numbers and no total count. This API is built for copying a workspace's data, and a cursor does that job better:
- Nothing is skipped or repeated. Lists are ordered by last change. With page numbers, a record edited while you page moves to the end of the list, every later record shifts back one place, and the next page silently misses one. A cursor continues after the last record you received, whatever changes in the meantime.
- Every page is as fast as the first. With page numbers, page 100 means reading and discarding the 99 pages before it. A cursor goes straight to where you left off, so a list of half a million rows pages at the same speed to the end.
- No count on every call. Counting a large list costs as much as reading it. Without a total, each request only reads the page it returns.
| You need | Do this |
|---|---|
| How many records there are | Page through /<resource>/ids (up to 5,000 ids a page) and count them. It is much cheaper than paging the full records. |
| A progress indicator during a sync | Count the ids first, then compare with the records received so far. |
| To resume after a failure | Send the last next_cursor you received again. It stays valid. |
| To open one record | GET /<resource>/{id}. |
Keeping a copy in sync
First load: page through each list endpoint without
updated_since. Deleted records are left out.Incremental sync: store the largest
updated_atyou received. On the next run, pass it asupdated_since. Records deleted since then come back in this short form, so you can remove them:json{ "id": "3f0c…", "deleted": true, "updated_at": "2026-10-02T14:05:11Z" }Find deletions
updated_sincemisses: some deletions are not recorded with a time and never show up inupdated_since. Engagifii deletes some records, such as interactions, outright. Every resource has an/idsendpoint for this case, for exampleGET /people/ids, except/interaction-participantsand/interaction-organizations(see Interactions). It returns the id of every live record:json{ "data": ["0a1b…", "0a1c…"], "next_cursor": "0a1c…" }page_sizedefaults to and maxes out at 5000. Passnext_cursorback ascursoruntil it isnull. Ids are not in alphabetical order, so collect every page before you compare. Delete any record in your copy whose id is not in the full list. Run this check now and then, for example nightly.
Some resource pages also say which changes do not move updated_at. For those resources, reload in full from time to time.
Errors
Errors follow RFC 9457 problem details:
json
{
"type": "about:blank",
"title": "Invalid request",
"status": 400,
"detail": "The selected status is invalid.",
"errors": { "status": ["The selected status is invalid."] },
"trace_id": "6c1f2a8e-0f3d-4b7a-9a52-2f7e5d1c9b10"
}| Status | When | Notes |
|---|---|---|
400 | A query parameter is invalid | errors lists the problem for each parameter. |
401 | The token is missing, wrong, expired or revoked | |
404 | No record with that id in this workspace, or an unknown path | |
429 | Rate limit exceeded | Wait the seconds given in Retry-After. |
500 | An error on Engagifii's side | No internal details are returned. Quote trace_id to support. |
503 | Engagifii took too long to answer | title is Temporarily unavailable, with Retry-After: 60. Wait that many seconds and repeat the same request, with the same cursor. Nothing is lost. /interactions/{id}/body has its own 503 cases. |
Every error carries a trace_id. Include it when you report a problem.
