Access Events Resource
Table of Contents
Overview
The access events resource exports your organization’s audit log as structured JSON, so a SIEM, data lake or other tool can collect it automatically, without anyone opening the web app.
Each event records who acted (the member or system, and the source IP address), what they did, where (the organization and the member or resource affected), when (a UTC timestamp) and the result (whether it succeeded, failed or was blocked, and why).
List Access Events
Fetch a page of access events for an organization, newest first by default.
Request
Method: GET
URL: https://api.ghostinspector.com/v1/organizations/{{organizationId}}/access-events?apiKey={{apiKey}}
Query parameters
apiKey- Your API key provided in your account
organizationId- The ID of the organization
count- The number of events to return, a whole number from 1 to 1000 (default 100)
cursor- Continue after the last event of a previous page. Pass the pagination.nextCursor value from that response, with the same order.
order- desc (newest first, the default) or asc (oldest first)
action- Only events with this exact action, for example user.login.blocked
actor- Only events performed by this user ID
actorType- Only events performed by this kind of actor: user, admin (Ghost Inspector staff), scim or system
status- Only events with this result: success, failure or blocked
createdAfter- Only events at or after this time (see Time ranges below)
createdBefore- Only events at or before this time (see Time ranges below)
targetEmail- Only events whose target email address contains this text (not case-sensitive)
{"code": "SUCCESS","data": [{"_id": "66e2f1a4c3b2a10012ab34cd","createdAt": "2026-09-15T14:03:22.114Z","organization": "5f2a9c7e1b4d3a0011223344","action": "user.login.blocked","category": "authentication","status": "blocked","reason": "sso-required","actor": {"type": "user","userId": "5f2a9c7e1b4d3a0011990011","email": "member@example.com","ipAddress": "203.0.113.4","channel": "app","credentialId": null},"target": {"type": "user","userId": "5f2a9c7e1b4d3a0011990011","email": "member@example.com","resourceId": null,"name": null},"aggregation": null,"details": {"method": "local"}}],"pagination": {"count": 1,"hasMore": true,"nextCursor": "eyJ0IjoiMjAyNi0wOS0xNVQxNDowMzoyMi4xMTRaIi..."}}
Invalid parameters (an unrecognized value, a count outside 1–1000, a malformed date or cursor, a cursor used with a different order, or a parameter given more than once) return a VALIDATION_ERROR. See Errors.
Event fields
Every event has the same fields. A field that doesn’t apply to an event is null, never missing.
| Field | Description |
|---|---|
_id | Unique ID of the event |
createdAt | When the event happened, as an ISO 8601 timestamp in UTC |
organization | ID of the organization the event belongs to |
action | What happened. See Event types |
category | The kind of activity: authentication, administrative or data_access |
status | The result: success, failure (attempted and failed, for example a wrong password) or blocked (refused by a rule, for example the IP allowlist) |
reason | Why the event failed or was blocked, when known (for example invalid-credentials or ip-not-allowlisted) |
actor.type | Who performed the action: user (a member or other Ghost Inspector user), admin (Ghost Inspector staff), scim (your identity provider, through SCIM) or system |
actor.userId | ID of the user who performed the action |
actor.email | Current email address of that user. null for SCIM and system actors, for Ghost Inspector staff, and for users whose account has since been deleted (actor.userId is kept) |
actor.ipAddress | IP address the request came from |
actor.channel | How the request arrived: app, api, mcp (an AI agent), extension, scim or system. null if it wasn’t recorded |
actor.credentialId | Which credential was used, by ID: an AI agent token’s ID, or primary or secondary for a SCIM token. Never the credential itself |
target.type | Kind of thing affected, for example user, organization, scimToken, test or dataSource |
target.userId | ID of the member affected, where there is one |
target.email | Email address of the member affected, as recorded at the time |
target.resourceId | ID of the resource affected, where there is one (for example the test that was exported) |
target.name | Readable name of the affected item, for example a test’s name, where one is known |
aggregation | On summary events, how many occurrences the event stands for. null on every event recorded individually |
details | Extra information that depends on the action. Its contents may change; build rules on action and status instead |
These fields answer the "who, what, where, when and result" questions an audit review asks:
| Question | Fields |
|---|---|
| Who | actor.type, actor.userId, actor.email, actor.ipAddress, actor.channel, actor.credentialId |
| What | action, category |
| Where | organization, target.type, target.userId, target.email, target.resourceId, target.name |
| When | createdAt |
| Result | status, reason |
Summary events
Some actions can repeat many times a minute: a CI pipeline reading your tests, or a client retrying from a blocked address. These are recorded as summaries. A summary stands for every matching occurrence in a five-minute window: the same action, person or credential, IP address, kind of target, result and reason (and, for reads, the same endpoint). Nothing is sampled: aggregation.eventCount counts every occurrence. Any event with an aggregation object is a summary.
| Action | Recorded as a summary |
|---|---|
data.read.summary | Always. Exports and data source reads are never summarized; each one is its own data.exported or data.read event |
admin.organization.accessed | Always. Requests to different endpoints share one summary, and details.routes lists those endpoints |
user.login.blocked | Only when reason is rate-limited (repeated attempts after the sign-in limit). A sign-in refused because SSO is required (sso-required) is always its own event |
user.request.blocked | Always |
api.request.failed | Always |
mcp.session.failed | Always |
mcp.session.blocked | Always |
mcp.request.failed | Always |
mcp.request.blocked | Always |
scim.request.failed | Always |
scim.request.blocked | Always |
Every other action is recorded individually, so its aggregation is null. Older events only: an event recorded before summaries were introduced may be individual, with aggregation set to null, even if its action is listed above.
| Field | Description |
|---|---|
aggregation.eventCount | The exact number of occurrences this event stands for |
aggregation.windowStart | Start of the five-minute window, in UTC |
aggregation.windowEnd | End of the five-minute window, in UTC |
aggregation.resourceIds | IDs of the resources read, up to 50. Empty on events that aren't reads |
aggregation.resourceIdsTruncated | true if more than 50 resources were read, so resourceIds isn’t the full list |
{"action": "data.read.summary","category": "data_access","status": "success","aggregation": {"eventCount": 4,"windowStart": "2026-09-15T14:00:00.000Z","windowEnd": "2026-09-15T14:05:00.000Z","resourceIds": ["66e2f1a4c3b2a10012ab34ce"],"resourceIdsTruncated": false}}
A summary appears as soon as its first occurrence is recorded, and its count keeps growing until about a minute after aggregation.windowEnd. Following a cursor doesn’t return an event twice, so a collector that reads a summary before then keeps the lower count. To store final counts, see Collecting events continuously.
Pagination
Results are paged with a cursor. Every response includes a pagination object:
count: the number of events in this response.hasMore:trueif more events match right now.nextCursor: the position just after the last event returned. Pass it ascursorto get the next page.
The order is fixed: by time, with ties broken consistently. Following nextCursor therefore returns every matching event exactly once, even while new events are being recorded. Don’t page by adjusting createdAfter or createdBefore yourself; events that share a timestamp can be skipped or repeated that way.
A cursor belongs to the order it was returned for: using it with the other order returns a VALIDATION_ERROR, because it would walk back over events you already have.
nextCursor is returned on the last page too, and a request with no new events hands the same cursor back. Save it and your next request picks up exactly where the last one stopped.
Collecting events continuously
To feed a SIEM or data lake, poll oldest-first:
- First run: request
order=ascwithcreatedAfterset to the earliest time you want, for example2026-01-01. Leave it out to start from the beginning of your organization’s log. - Keep requesting with
cursorset to the latestnextCursoruntilhasMoreisfalse. - Save the latest
nextCursor. - Every following run (for example, every 5 minutes): request
order=asc&cursor=<saved cursor>, then repeat steps 2–3.
To store final counts for summary events, add createdBefore set to about 10 minutes ago on every request, so a summary is only collected once its window has closed. Keep the same saved cursor.
curl -G "https://api.ghostinspector.com/v1/organizations/$ORG_ID/access-events" \
-d "apiKey=$API_KEY" \
-d "order=asc" \
-d "count=1000" \
--data-urlencode "cursor=$CURSOR"
import requests
URL = f"https://api.ghostinspector.com/v1/organizations/{org_id}/access-events"
def collect(cursor=None):
"""Return every event after `cursor`, and the cursor to save for the next run."""
events = []
while True:
params = {"apiKey": api_key, "order": "asc", "count": 1000}
if cursor:
params["cursor"] = cursor
body = requests.get(URL, params=params, timeout=30).json()
if body["code"] != "SUCCESS":
raise RuntimeError(body["message"])
events.extend(body["data"])
cursor = body["pagination"]["nextCursor"]
if not body["pagination"]["hasMore"]:
return events, cursor
events, cursor = collect(saved_cursor) # saved_cursor is None on the first run
send_to_siem(events)
save(cursor)
Time ranges
createdAfter and createdBefore are inclusive and accept:
- a date, such as
2026-09-01, meaning midnight UTC at the start of that day, or - a date and time with
Zor a UTC offset, such as2026-09-01T14:00:00Zor2026-09-01T16:00:00+02:00.
A date and time without Z or an offset is rejected, because it would be ambiguous. All timestamps in responses are UTC.
Because a date means the start of that day, createdBefore=2026-06-30 leaves out June 30 itself. To include all of June 30, use createdBefore=2026-06-30T23:59:59.999Z.
Event types
These actions are recorded today, grouped by category. An action’s usual status is shown here; a few actions can record a different one, so always read status from the event.
Authentication
| Action | Status | Description |
|---|---|---|
user.login | success | A member signed in (password, SSO, Google/GitHub or the browser extension) |
user.login.failed | failure | A sign-in failed, for example a wrong password or an invalid two-factor code |
user.login.blocked | blocked | A sign-in was refused because SSO is required, or after too many attempts. Older events only: reason can also be ip-not-allowlisted, from when the IP allowlist refused sign-ins |
user.request.blocked | blocked | A web or API request was refused by the IP allowlist |
user.apiKey.issueBlocked | blocked | Older events only: the browser extension's sign-in was refused by the IP allowlist. It's no longer refused |
user.recoveryCode.used | success | A two-factor recovery code was used to sign in |
api.request.failed | failure | An API request used the key of a disabled account |
mcp.session.started | success | An AI agent (MCP) connected |
mcp.session.failed | failure | An AI agent failed to connect, for example with an invalid token |
mcp.session.blocked | blocked | An AI agent’s connection was refused by the IP allowlist |
mcp.request.failed | failure | A request from an AI agent was refused, for example because AI agent access is turned off |
mcp.request.blocked | blocked | A request from an AI agent was refused by the IP allowlist |
scim.request.failed | failure | A SCIM request from your identity provider failed, for example with an invalid token |
scim.request.blocked | blocked | A SCIM request from your identity provider was refused by the IP allowlist |
Administrative changes
| Action | Status | Description |
|---|---|---|
member.invited | success | Someone was invited to the organization |
member.invite.revoked | success | An invitation was revoked |
member.added | success | An existing user was added to the organization |
member.roleChanged | success | A member was made an administrator, or stopped being one |
member.removed | success | A member was removed from the organization |
organization.sso.updated | success | Single sign-on settings changed |
organization.scim.updated | success | SCIM provisioning was turned on or off |
organization.ipAllowlist.updated | success | The IP allowlist changed |
organization.variables.updated | success | Organization variables changed. Names only, never values |
organization.cancelled | success | The organization’s subscription was cancelled |
suite.variables.updated | success | A suite’s variables changed. Names only, never values |
dataSource.created | success | A data source was created |
dataSource.updated | success | A data source was changed |
dataSource.deleted | success | A data source was deleted |
scim.token.generated | success | A SCIM token was generated |
scim.token.revoked | success | A SCIM token was revoked |
scim.token.secondary.generated | success | A secondary SCIM token was generated for rotation |
scim.token.secondary.promoted | success | The secondary SCIM token replaced the primary |
scim.token.secondary.revoked | success | The secondary SCIM token was revoked |
scim.user.provisioned | success | Your identity provider added a member |
scim.user.deactivated | success | Your identity provider deactivated a member |
scim.user.reactivated | success | Your identity provider reactivated a member |
scim.user.deprovisioned | success | Your identity provider removed a member |
mcp.token.created | success | An AI agent token was created |
mcp.token.revoked | success | An AI agent token was revoked |
mcp.settings.updated | success | AI agent (MCP) access settings changed |
admin.mcpAccess.granted | success | Ghost Inspector staff turned on AI agent access for the organization |
admin.mcpAccess.revoked | success | Ghost Inspector staff turned off AI agent access for the organization |
user.password.changed | success | A member changed their password |
user.password.reset | success | A member reset their password by email |
user.email.changed | success | A member changed their email address |
user.2fa.enabled | success | A member turned on two-factor authentication |
user.2fa.disabled | success | A member turned off two-factor authentication |
user.apiKey.regenerated | success | A member generated a new API key |
Data access
| Action | Status | Description |
|---|---|---|
data.read.summary | success | Tests, suites, results, folders or organization details were read through the API, an AI agent or the browser extension. See Summary events |
data.read | success | The organization’s data sources were read. One event per request |
data.exported | success | A test or suite was exported, or results were exported as CSV or xUnit. One event per request |
admin.organization.accessed | success | A member of Ghost Inspector staff viewed the organization |
A data read that is refused, such as someone outside your organization requesting one of your tests, is recorded in your organization with status set to blocked.
Retention
Access events are kept for as long as your organization exists. They aren’t affected by your plan’s test result retention period, and the full history is available through this endpoint.
Rate limits
Requests count toward the standard API rate limits. A page of up to 1,000 events is one request, so even a busy organization can be collected every few minutes well within the limits.