Skip to content

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 are application/problem+json.

Base URL ​

text
https://{api-host}/api/v1

All paths on these pages are relative to the base URL. For example, GET /people means GET https://{api-host}/api/v1/people.

Pages ​

PageResources
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 /whoami returns token_expires_at.
  • Issuing a new token for an integration cancels its old token straight away.
  • The token is accepted only in the Authorization header, 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_id on an invoice is the id from /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 null when they have no value. A few, such as record numbers and names, can come back as "", so treat "" and null the 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-participants and /interaction-organizations are 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.

ParameterMeaning
page_sizeRecords 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.
cursorThe next_cursor from the previous page, passed back unchanged.
updated_sinceOnly 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 needDo this
How many records there arePage 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 syncCount the ids first, then compare with the records received so far.
To resume after a failureSend the last next_cursor you received again. It stays valid.
To open one recordGET /<resource>/{id}.

Keeping a copy in sync ​

  1. First load: page through each list endpoint without updated_since. Deleted records are left out.

  2. Incremental sync: store the largest updated_at you received. On the next run, pass it as updated_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" }
  3. Find deletions updated_since misses: some deletions are not recorded with a time and never show up in updated_since. Engagifii deletes some records, such as interactions, outright. Every resource has an /ids endpoint for this case, for example GET /people/ids, except /interaction-participants and /interaction-organizations (see Interactions). It returns the id of every live record:

    json
    { "data": ["0a1b…", "0a1c…"], "next_cursor": "0a1c…" }

    page_size defaults to and maxes out at 5000. Pass next_cursor back as cursor until it is null. 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"
}
StatusWhenNotes
400A query parameter is invaliderrors lists the problem for each parameter.
401The token is missing, wrong, expired or revoked
404No record with that id in this workspace, or an unknown path
429Rate limit exceededWait the seconds given in Retry-After.
500An error on Engagifii's sideNo internal details are returned. Quote trace_id to support.
503Engagifii took too long to answertitle 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.