Skip to content

Audit Logs

The audit log records changes made in your Shorebird organization: who created a release, promoted or rolled back a patch, changed a member’s role, issued a service account key, and so on. Each entry says who acted, with which credential, from where, and on what.

People signing in, personal API keys, and requests that were refused aren’t recorded as events of their own. Each event names the sign-in session or API key behind it instead.

The audit log is available on the Enterprise plan. Owners and Admins of the organization can read it, as can a service account with the Auditor role. Other roles cannot. An enterprise’s Owners and Admins, and an Auditor service account the enterprise owns, read its own log: changes to the enterprise, and membership and access changes in its organizations, without their apps’ contents. See Enterprise Roles and Export your enterprise audit log.

Events are available through the API for at least 90 days.

In the Shorebird console, open your organization’s Settings and select the Audit Log tab to see every event in the organization, newest first. Each app also has an Audit Log tab that shows only that app’s events.

Both pages can be filtered by event. Open an entry’s details to see the credential used, the API key or sign-in session, the IP address, the user agent, and any event-specific details such as the patch number.

To keep the audit log in your own security tooling, poll the audit log API in oldest-first order and forward each new event.

  1. Create a service account and change its role to Auditor. Auditor can read the audit log and nothing else, so the key your SIEM holds cannot change anything in the organization.
  2. Create a key for it and send the key (sb_api_...) as a bearer token.
  3. Call GET /api/v1/organizations/{organizationId}/audit-logs?order=asc. The organization ID is the number after /orgs/ in the console’s address bar.
  4. Forward the events in data, then save the response’s next_cursor.
  5. On the next poll, send the saved value as cursor. Keep the cursor you have when a response has no next_cursor.

A poll without a cursor starts at the oldest event available. To start later, add startDate.

Oldest-first reads leave out events from the most recent minute, so an event whose write was still finishing when you polled is never skipped. It appears on a later poll instead.

If your poller stops after forwarding a page but before saving its cursor, the next poll returns those events again. Deduplicate on each event’s metadata.uid, which is unique per event.

Each page holds up to 100 events (per_page, default 30). When a page comes back full, poll again right away. Requests count against the API’s per-caller rate limit, and a 429 response carries a Retry-After header.

Save this as a script and run it on a schedule. Each run reads one page, appends its events to events.jsonl, and saves the cursor for the next run:

#!/usr/bin/env bash
set -euo pipefail
ORG_ID=1234

query=(--data-urlencode order=asc --data-urlencode per_page=100)
if [ -f cursor.txt ]; then
  query+=(--data-urlencode "cursor=$(cat cursor.txt)")
fi

curl -sf -G "https://api.shorebird.dev/api/v1/organizations/$ORG_ID/audit-logs" \
  -H "Authorization: Bearer $SHOREBIRD_TOKEN" \
  "${query[@]}" > page.json

jq -c '.data[]' page.json >> events.jsonl
next=$(jq -r '.next_cursor // empty' page.json)
if [ -n "$next" ]; then echo "$next" > cursor.txt; fi

shorebird api (Shorebird CLI 1.6.129 or later) sends the same request, authenticated with the key in SHOREBIRD_TOKEN. It replaces the query and curl lines in the script above:

query=(-X GET "organizations/$ORG_ID/audit-logs" -f order=asc -f per_page=100)
if [ -f cursor.txt ]; then
  query+=(-f "cursor=$(cat cursor.txt)")
fi
shorebird api "${query[@]}" > page.json

Don’t add --paginate (Shorebird CLI 1.6.130 or later) to an order=asc request: oldest-first responses keep returning a next_cursor, so once --paginate reaches the newest event it sees the same cursor twice, exits with status 1, and prints no events.

shorebird audit-log (Shorebird 1.6.135 or newer) reads the same events. list shows one page and prints the command for the next one. export writes every page to stdout, as one JSON array or, with --format jsonl, one event per line.

# One page, newest first, one line per event
shorebird audit-log list --org-id $ORG_ID

# Every event in October, as one JSON array
shorebird audit-log export --org-id $ORG_ID \
  --start 2026-10-01 --end 2026-10-31 > audit-2026-10.json

Both take the filters the API does, as flags such as --start, --end, --app-id, and --event. --start and --end also take a date, which covers the whole day in UTC, or how long ago, such as 7d. Run shorebird audit-log export --help for the options.

To read your enterprise’s own log instead, as one of its Owners or Admins, run shorebird enterprise audit-log (Shorebird 1.6.136 or newer). It takes the same list and export subcommands and filters, without an organization ID.

To poll on a schedule, export oldest first with --order asc and pass the cursor the previous run printed as --cursor, the way the scripts above save next_cursor. Without a cursor, each run starts again at the oldest event. With --json, the cursor is the envelope’s data.next_cursor, next to the events in data.entries.

It works with a shorebird login session, a key in SHOREBIRD_TOKEN, or GitHub Actions OIDC, as long as the credential belongs to an Owner, an Admin, or an Auditor service account. A release and patch only key can’t read the audit log.

Your enterprise’s own log, with changes to the enterprise and membership and access changes across its organizations, is exported the same way from a service account the enterprise owns. Enterprise Owners and Admins can create one:

  1. In the Shorebird console, open Enterprise in the sidebar, then its Security tab.
  2. Under Service accounts, click Create service account, name it for where the events will go, and create it. It gets the Auditor role, which reads the enterprise’s audit log and nothing else.
  3. Create a key for it and send the key as a bearer token.
  4. Poll GET /api/v1/enterprises/{enterpriseId}/audit-logs?order=asc the way the steps in Export the audit log to a SIEM describe, with the same cursor, filters and paging. shorebird enterprise prints your enterprise’s ID as (id: ...).

The account belongs to the enterprise, not to whoever created it, so the export keeps working when they leave.

These query parameters narrow the results, in either order:

  • startDate and endDate: ISO 8601 timestamps with a time zone, such as 2026-10-01T00:00:00Z. Both ends are inclusive.
  • appId: only events for that app.
  • actorId: only events by that user or service account (its actor.user.uid). Events by a user or service account that was later deleted no longer have a uid, so this filter doesn’t find them. It never matches events by Shorebird staff.
  • operation: only one event, by name, such as patch.rollback. See Events.
  • order: desc (newest first, the default) or asc.

For example, to list the patch rollbacks in an app, newest first:

shorebird api -X GET organizations/$ORG_ID/audit-logs \
  -f appId=$APP_ID -f operation=patch.rollback

Each event is an OCSF 1.9.0 API Activity event (class_uid 6003), so a SIEM that reads OCSF can ingest it without a custom parser. This is a member rolling back a patch in the console:

{
  "class_uid": 6003,
  "category_uid": 6,
  "activity_id": 3,
  "type_uid": 600303,
  "time": 1792180800000,
  "severity_id": 1,
  "status_id": 1,
  "metadata": {
    "version": "1.9.0",
    "product": { "name": "Shorebird", "vendor_name": "Shorebird" },
    "uid": "48213",
    "tenant_uid": "1234",
    "log_name": "audit"
  },
  "api": { "operation": "patch.rollback" },
  "actor": {
    "user": {
      "uid": "5678",
      "name": "jane@example.com",
      "email_addr": "jane@example.com",
      "type_id": 1,
      "type": "User"
    },
    "session": {
      "uid": "9012",
      "created_time": 1792170000000,
      "issuer": "https://accounts.google.com",
      "is_mfa": true
    }
  },
  "src_endpoint": { "ip": "203.0.113.7" },
  "http_request": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
  },
  "resources": [{ "type": "patch", "uid": "3456" }],
  "unmapped": {
    "patch_number": "2",
    "release_version": "1.4.0+12",
    "app_name": "Example App",
    "app_id": "0f3c9a52-6d1e-4b7a-9c1f-2e8d4a6b7c90",
    "actor_source": "session"
  }
}

The fields most queries use:

  • api.operation: the event name, from the list below. activity_id says whether it created (1), read (2), updated (3), or deleted (4) something, or 99 for anything else.
  • time: when the event happened, in milliseconds since the Unix epoch.
  • actor.user: who acted. type_id is 1 for a person and 4 for a service account, whose name is the account’s name. A service account has no email address, so it has no email_addr; identify it by uid. A change Shorebird staff made to your organization has type_id 99 and type shorebird_staff, with the name Shorebird staff. Which staff member acted isn’t shown, and neither is their credential, session, IP address, or user agent.
  • actor.session: present when the actor was signed in to the console or the CLI. uid identifies the sign-in session, created_time is when it began, and issuer is the identity provider the person signed in with. is_mfa is present only when the identity provider reported whether a second factor was used. Google reports it only when the person authenticated with Google during that sign-in, not when Google reused an existing Google session, so a missing is_mfa means unknown, not “no”.
  • api.token: present when the request used an API key. uid is the key’s ID, which is what you revoke, and name is the key’s name when the event was recorded.
  • src_endpoint.ip: the IP address the request came from. For an action in the console, that’s the person’s own address, not the console’s. When none was recorded, src_endpoint is {"name": "unknown"}.
  • http_request.user_agent: the request’s User-Agent, when it sent one. For an action in the console, that’s the person’s browser.
  • resources: what was acted on. type names the kind of thing uid identifies, such as patch, release, or user.
  • metadata.uid: the event’s unique ID. metadata.tenant_uid is the organization ID, and metadata.correlation_uid, when present, is the X-Request-ID header the request sent.
  • unmapped: fields OCSF has no place for. app_id and app_name name the app, actor_source names the kind of credential (session, api_key, service_account, federated for a CI job signed in with OIDC, admin_api for Shorebird staff, or none; absent on older events), and the rest are details of the event, such as a patch number or a member’s previous role. An org_member.add for a person who joined the organization automatically when they signed in has saml_sso, google_workspace, or microsoft_entra as its actor_source, for the identity provider they signed in with. A request from a CI job signed in with a trust rule has ci, with the repository, the run_id of the workflow run, and the rule_id of the trust rule.

Event names can change. A renamed event appears under its new name, including past occurrences. An event Shorebird no longer records appears with its stored resource and action as api.operation, in the form resource_type.action, and activity_id 99. The operation filter takes event names, not that fallback form.

Changes to a person’s own account, such as their display name, billing limits, or deleting their account, aren’t part of an organization’s audit log.

  • organization.create: an organization was created.
  • organization.rename: an organization was renamed.
  • organization.delete: deleting an organization and its apps stopped partway. Once an organization is fully deleted, its audit log can no longer be read.
  • organization.ownership_transfer: ownership was transferred to another member.
  • org_allowed_domain.add: Shorebird staff added an email domain to the domains the organization can invite from.
  • org_allowed_domain.remove: Shorebird staff removed an email domain from those domains.
  • organization.enterprise_default_update: an enterprise admin or Shorebird staff made the organization one of the enterprise’s default organizations, changed the role people join it at, or stopped it being one. Its details are the enterprise ID and the role before and after.
  • org_member.add: a user joined the organization.
  • org_member.role_update: a member’s role was changed.
  • org_member.remove: a member was removed.
  • invitation.create: an invitation to the organization or an app was sent.
  • invitation.revoke: a pending invitation was revoked.
  • invitation.accept: an invitation was accepted.
  • invitation.decline: an invitation was declined.
  • service_account.create: a service account was created.
  • service_account.role_update: a service account’s role was changed.
  • service_account.delete: a service account and its API keys were deleted.
  • service_account_key.create: an API key was issued to a service account.
  • service_account_key.revoke: a service account’s API key was revoked.
  • service_account_trust_rule.create: a trust rule was added to a service account.
  • service_account_trust_rule.delete: a trust rule was deleted.
  • service_account.sign_in: a CI job signed in as a service account through a trust rule. Its unmapped details are the job’s claims, such as repository, workflow_ref, run_id, and ref, with the trust rule’s rule_id. They aren’t under ci.
  • app.create: an app was created.
  • app.update: an app’s settings, such as its display name, were changed.
  • app.transfer: an app was moved to another organization.
  • app.delete: an app was deleted.
  • app_collaborator.add: a user was given a role on an app.
  • app_collaborator.role_update: an app collaborator’s role was changed.
  • app_collaborator.remove: a collaborator was removed from an app.
  • app_store_connect_credential.set: the organization gave Shorebird an App Store Connect key, replacing any it had.
  • app_store_connect_credential.delete: the organization removed its App Store Connect key.
  • channel.create: a channel was created.
  • channel.delete: a channel was deleted.
  • signing_key.create: Shorebird created the organization’s managed patch-signing key. It does this when a signing key is fetched for one of the organization’s apps and the organization has no current key, as on its first managed shorebird release. The actor is whoever made that request.
  • release.create: a release was created.
  • release.update: a release was updated, such as its status or notes.
  • release.delete: a release was deleted.
  • release_artifact.upload: a release artifact was uploaded.
  • release_artifact.replace: a release artifact was replaced by a new upload.
  • release_artifact.delete: a release’s artifacts were deleted.
  • release_artifact.download: a release’s artifacts were listed with their download links.
  • patch.create: a patch was created.
  • patch_artifact.upload: a patch artifact was uploaded.
  • patch.promote: a patch was promoted to the stable channel.
  • patch.channel_update: a patch was moved to a channel other than stable.
  • patch.rollback: a patch was rolled back.
  • patch.rollforward: a rolled-back patch was rolled forward.
  • patch.notes_update: a patch’s notes were changed.

These appear in an enterprise’s log. Events about an organization’s place in the enterprise, and revoking an organization’s service account key, also appear in that organization’s log.

  • enterprise.create: the enterprise was created.
  • enterprise.rename: the enterprise was renamed.
  • enterprise.billing_account_update: Shorebird staff gave the enterprise its billing account, so its organizations bill through the company.
  • enterprise_contract.download: an Owner or Billing manager downloaded one of the enterprise’s signed contracts.
  • enterprise.organization_attach: an organization was added to the enterprise.
  • enterprise.organization_detach: an organization was removed from the enterprise, keeping its apps and members.
  • enterprise_domain.add: a domain was added to the enterprise. People who sign in with an account managed by the domain are in the enterprise’s directory from then on.
  • enterprise_domain.remove: a domain was removed from the enterprise.
  • enterprise_domain.sign_in_method_update: the sign-in method a domain’s accounts must use was set or cleared.
  • enterprise_saml_connection.attach: a SAML connection was attached to the enterprise.
  • enterprise_saml_connection.detach: a SAML connection was detached.
  • enterprise.guest_policy_update: the enterprise changed who outside its directory its organizations may invite: anyone, no one, or people at listed domains. Guests already in its organizations stay.
  • enterprise_member.add: an admin, or the enterprise’s identity provider over SCIM, added a person before their first sign-in, with an enterprise role.
  • enterprise_member.role_update: a person’s enterprise role was changed.
  • enterprise_member.update: the enterprise’s identity provider changed a member’s email, display name, or its own ID for them over SCIM.
  • enterprise_member.remove: an admin, or the enterprise’s identity provider over SCIM, deactivated a person: removed them from every one of the enterprise’s organizations and every grant on its apps, and revoked their sessions, API keys, and invitations. Each organization also records an org_member.remove.
  • enterprise_member.reactivate: a deactivated person was reactivated, so they can sign in again with the enterprise role they held. Their organizations, grants, sessions, and API keys don’t come back.
  • enterprise_guest.remove: an admin removed a guest, someone the enterprise doesn’t manage, from all of its organizations and app grants, and revoked their invitations. The guest’s account is left alone. Each organization also records an org_member.remove.
  • enterprise_api_key.revoke: an admin revoked an API key from the enterprise’s inventory: a service account’s key or a member’s personal key.