Access Events Resource

Table of Contents

  • Overview

  • List Access Events

  • Event fields

  • Summary events

  • Pagination

  • Time ranges

  • Event types

  • Retention

  • Rate limits

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).

Only organization administrators can export access events. Authenticate with an administrator’s API key. AI agent (MCP) tokens can’t read access events. If your organization uses an IP allowlist, it applies to these requests too.

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.

FieldDescription
_idUnique ID of the event
createdAtWhen the event happened, as an ISO 8601 timestamp in UTC
organizationID of the organization the event belongs to
actionWhat happened. See Event types
categoryThe kind of activity: authentication, administrative or data_access
statusThe result: success, failure (attempted and failed, for example a wrong password) or blocked (refused by a rule, for example the IP allowlist)
reasonWhy the event failed or was blocked, when known (for example invalid-credentials or ip-not-allowlisted)
actor.typeWho performed the action: user (a member or other Ghost Inspector user), admin (Ghost Inspector staff), scim (your identity provider, through SCIM) or system
actor.userIdID of the user who performed the action
actor.emailCurrent 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.ipAddressIP address the request came from
actor.channelHow the request arrived: app, api, mcp (an AI agent), extension, scim or system. null if it wasn’t recorded
actor.credentialIdWhich credential was used, by ID: an AI agent token’s ID, or primary or secondary for a SCIM token. Never the credential itself
target.typeKind of thing affected, for example user, organization, scimToken, test or dataSource
target.userIdID of the member affected, where there is one
target.emailEmail address of the member affected, as recorded at the time
target.resourceIdID of the resource affected, where there is one (for example the test that was exported)
target.nameReadable name of the affected item, for example a test’s name, where one is known
aggregationOn summary events, how many occurrences the event stands for. null on every event recorded individually
detailsExtra 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:

QuestionFields
Whoactor.type, actor.userId, actor.email, actor.ipAddress, actor.channel, actor.credentialId
Whataction, category
Whereorganization, target.type, target.userId, target.email, target.resourceId, target.name
WhencreatedAt
Resultstatus, reason
New actions and target types are added as Ghost Inspector records more kinds of activity. Your integration should accept values it hasn’t seen before rather than reject them.

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.

ActionRecorded as a summary
data.read.summaryAlways. Exports and data source reads are never summarized; each one is its own data.exported or data.read event
admin.organization.accessedAlways. Requests to different endpoints share one summary, and details.routes lists those endpoints
user.login.blockedOnly 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.blockedAlways
api.request.failedAlways
mcp.session.failedAlways
mcp.session.blockedAlways
mcp.request.failedAlways
mcp.request.blockedAlways
scim.request.failedAlways
scim.request.blockedAlways

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.

FieldDescription
aggregation.eventCountThe exact number of occurrences this event stands for
aggregation.windowStartStart of the five-minute window, in UTC
aggregation.windowEndEnd of the five-minute window, in UTC
aggregation.resourceIdsIDs of the resources read, up to 50. Empty on events that aren't reads
aggregation.resourceIdsTruncatedtrue 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: true if more events match right now.
  • nextCursor: the position just after the last event returned. Pass it as cursor to 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:

  1. First run: request order=asc with createdAfter set to the earliest time you want, for example 2026-01-01. Leave it out to start from the beginning of your organization’s log.
  2. Keep requesting with cursor set to the latest nextCursor until hasMore is false.
  3. Save the latest nextCursor.
  4. 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 Z or a UTC offset, such as 2026-09-01T14:00:00Z or 2026-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

ActionStatusDescription
user.loginsuccessA member signed in (password, SSO, Google/GitHub or the browser extension)
user.login.failedfailureA sign-in failed, for example a wrong password or an invalid two-factor code
user.login.blockedblockedA 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.blockedblockedA web or API request was refused by the IP allowlist
user.apiKey.issueBlockedblockedOlder events only: the browser extension's sign-in was refused by the IP allowlist. It's no longer refused
user.recoveryCode.usedsuccessA two-factor recovery code was used to sign in
api.request.failedfailureAn API request used the key of a disabled account
mcp.session.startedsuccessAn AI agent (MCP) connected
mcp.session.failedfailureAn AI agent failed to connect, for example with an invalid token
mcp.session.blockedblockedAn AI agent’s connection was refused by the IP allowlist
mcp.request.failedfailureA request from an AI agent was refused, for example because AI agent access is turned off
mcp.request.blockedblockedA request from an AI agent was refused by the IP allowlist
scim.request.failedfailureA SCIM request from your identity provider failed, for example with an invalid token
scim.request.blockedblockedA SCIM request from your identity provider was refused by the IP allowlist

Administrative changes

ActionStatusDescription
member.invitedsuccessSomeone was invited to the organization
member.invite.revokedsuccessAn invitation was revoked
member.addedsuccessAn existing user was added to the organization
member.roleChangedsuccessA member was made an administrator, or stopped being one
member.removedsuccessA member was removed from the organization
organization.sso.updatedsuccessSingle sign-on settings changed
organization.scim.updatedsuccessSCIM provisioning was turned on or off
organization.ipAllowlist.updatedsuccessThe IP allowlist changed
organization.variables.updatedsuccessOrganization variables changed. Names only, never values
organization.cancelledsuccessThe organization’s subscription was cancelled
suite.variables.updatedsuccessA suite’s variables changed. Names only, never values
dataSource.createdsuccessA data source was created
dataSource.updatedsuccessA data source was changed
dataSource.deletedsuccessA data source was deleted
scim.token.generatedsuccessA SCIM token was generated
scim.token.revokedsuccessA SCIM token was revoked
scim.token.secondary.generatedsuccessA secondary SCIM token was generated for rotation
scim.token.secondary.promotedsuccessThe secondary SCIM token replaced the primary
scim.token.secondary.revokedsuccessThe secondary SCIM token was revoked
scim.user.provisionedsuccessYour identity provider added a member
scim.user.deactivatedsuccessYour identity provider deactivated a member
scim.user.reactivatedsuccessYour identity provider reactivated a member
scim.user.deprovisionedsuccessYour identity provider removed a member
mcp.token.createdsuccessAn AI agent token was created
mcp.token.revokedsuccessAn AI agent token was revoked
mcp.settings.updatedsuccessAI agent (MCP) access settings changed
admin.mcpAccess.grantedsuccessGhost Inspector staff turned on AI agent access for the organization
admin.mcpAccess.revokedsuccessGhost Inspector staff turned off AI agent access for the organization
user.password.changedsuccessA member changed their password
user.password.resetsuccessA member reset their password by email
user.email.changedsuccessA member changed their email address
user.2fa.enabledsuccessA member turned on two-factor authentication
user.2fa.disabledsuccessA member turned off two-factor authentication
user.apiKey.regeneratedsuccessA member generated a new API key

Data access

ActionStatusDescription
data.read.summarysuccessTests, suites, results, folders or organization details were read through the API, an AI agent or the browser extension. See Summary events
data.readsuccessThe organization’s data sources were read. One event per request
data.exportedsuccessA test or suite was exported, or results were exported as CSV or xUnit. One event per request
admin.organization.accessedsuccessA 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.