Skip to content

Audit History API ​

The read side of the append-only audit log. The standalone viewer (GET /admin/audit-logs) and the per-entity history routes share one response shape. Rows are written once and never changed — a trigger rejects UPDATE and DELETE on the table.

Reads are backed by two composite indexes on the audit log: one on (entity_type, entity_id, created_at) for the entity history panels, and one on (actor_id, created_at) for actor-filtered views.

Entry Shape ​

json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-678901234567",
  "createdAt": "2026-06-10T12:00:00.000Z",
  "actor": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Sara Officer" },
  "action": "officer.delivery_email_resent",
  "entityType": "activation",
  "entityId": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "changedFields": ["customerId", "sequence"]
}
FieldTypeDescription
idstring (uuid)Audit row ID
createdAtstringISO 8601 timestamp
actorobject | nullThe user who triggered the event, null for system actions
actionstringThe event string, see Audit Actions
entityTypestringType of entity affected (e.g. activation, user, company)
entityIdstring (uuid)ID of the affected entity
changedFieldsstring[]Sorted union of the keys in oldValues and newValues
oldValuesobject | nullPrevious state. Owner only
newValuesobject | nullNew state. Owner only

Redaction Rule ​

Every viewer sees changedFields, so they can see what moved without seeing the values. oldValues and newValues are included only for the owner (super_admin). The rule is identical on the standalone viewer and every per-entity history route.

Pagination ​

Keyset (cursor-based) pagination over (createdAt, id) descending, the same pattern as the other admin lists.

ParameterTypeDefaultDescription
cursorstring(none)Full-precision createdAt|id from the previous page's nextCursor, e.g. 2026-06-10 12:00:00.123456+00|f6a7b8c9-d0e1-2345-fabc-678901234567
limitinteger50Page size from 1 to 100

The response envelope is { items, nextCursor, hasMore }. nextCursor is null on the last page.


GET /admin/audit-logs ​

The standalone audit viewer. Filter and paginate the whole log, newest first.

Requires the audit area (View).

Query Parameters ​

ParameterTypeDefaultDescription
entityTypestring(none)Restrict to one entity type
entityIdstring (uuid)(none)Restrict to one entity
actorIdstring (uuid)(none)Restrict to one actor
actionstring(none)Restrict to one action string
fromISO datetime(none)Inclusive lower bound on createdAt
toISO datetime(none)Inclusive upper bound on createdAt
cursorstring(none)createdAt|id cursor from the previous page
limitinteger50Page size from 1 to 100

Example ​

GET /admin/audit-logs?entityType=esim_store_order&action=store.order_requeued&limit=20
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response ​

200 OK

json
{
  "items": [
    {
      "id": "f6a7b8c9-d0e1-2345-fabc-678901234567",
      "createdAt": "2026-06-10T12:00:00.000Z",
      "actor": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Sara Officer" },
      "action": "store.order_requeued",
      "entityType": "esim_store_order",
      "entityId": "b8c9d0e1-f2a3-4567-bcde-890123456789",
      "changedFields": ["jobId", "orderId", "userId"],
      "oldValues": null,
      "newValues": {
        "orderId": "b8c9d0e1-f2a3-4567-bcde-890123456789",
        "userId": "e5f6a7b8-c9d0-1234-efab-567890123456",
        "jobId": "b8c9d0e1-f2a3-4567-bcde-890123456789-fulfill"
      }
    }
  ],
  "nextCursor": "2026-06-10T12:00:00.000Z|f6a7b8c9-d0e1-2345-fabc-678901234567",
  "hasMore": true
}

Error Responses ​

CodeStatusCondition
VALIDATION_ERROR400Invalid UUID, datetime, cursor, or limit
AUTH_REQUIRED401Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED)
AUTH_FORBIDDEN403Caller does not hold the audit area

Entity History Routes ​

Each returns the audit rows for one entity, newest first, in the same { items, nextCursor, hasMore } envelope. All accept only cursor and limit as query parameters, under the pagination rules above. The redaction rule applies everywhere: changedFields for every viewer, oldValues/newValues for the owner.

RouteEntity typeAccess
GET /admin/customers/:id/historyuserThe customers area (View) — see Customers
GET /admin/store/orders/:id/historyesim_store_orderThe store area (View) — see eSIM Store
GET /admin/companies/:id/historycompanyRole + b2b View, the same gate as the rest of the company oversight routes — see B2B Admin

Audit Actions ​

The action strings the app writes today. For staff user changes, company registration, company invites, roster changes, link minting, quote counters, contract confirmation, contract payments and activation creation, the row is written in the same transaction as the change, so the change and its row commit together or not at all.

Activation lifecycle ​

ActionMeaning
activation.iccid_validatedAn ICCID or LPA code was validated against an operator (public route, no actor)
activation.createdAn activation order was created. Values carry the order id, and the company id for a B2B activation
activation.identity_initiatedIdentity verification was initiated. A status check that is still waiting writes no row
activation.identity_completedA verification status check returned completed. Written once per identity attempt, however often the app polls
activation.identity_rejectedA verification status check returned rejected. Written once per identity attempt, however often the app polls
activation.identity_expiredA verification status check returned expired. Written once per identity attempt, however often the app polls
activation.identity_errorA verification status check returned error. Written once per identity attempt, however often the app polls

Staff and administration ​

ActionMeaning
admin.user.createdAn administration user was provisioned; records the role, managedAreas and managedAreasWrite, plus the company id and company role for a company user
admin.user.updatedAn administration user's role, areas, or active state changed; records the old and new role, areas, managedAreasWrite and active state, and is skipped when nothing changed
admin.user.deletedAn administration user was removed. Old values carry the role, areas, managedAreasWrite and active state, plus the company id and company role for a company user
admin.user.invitedA sign-in invitation trigger was accepted for an administration user
admin.plugin.payment.updatedThe checkout-wide "accept new payments" setting was updated
admin.plugin.moyasar.updatedThe Moyasar plugin settings were updated
admin.plugin.paymob.updatedThe Paymob plugin settings were updated (key-presence flags only, no keys)
admin.plugin.email.updatedThe email plugin settings were updated
admin.plugin.mobimatter.updatedThe Mobimatter plugin settings were updated
admin.links_exportThe cross-company magic-link oversight CSV was exported
customer.identity_viewedA staff member opened a customer's identity check detail
customer.viewedA staff member opened a customer's record. Written on entity type customer_record (the customer id), so it stays out of the customer's own history. A repeat view by the same staff member within 10 minutes writes no new row
customer.searchedA staff member searched customers. Values carry the term's kind, the result count and the portal's search id, never the term. The portal sends a new search id on each submit; a re-run of the same search id by the same staff member within 10 minutes writes no new row, and a search without an id always writes
officer.completions_searchedAn officer searched completions by phone or serial. Values carry the term's kind, the result count and the portal's search id, never the term. The portal sends a new search id on each submit; a re-run of the same search id by the same officer within 10 minutes writes no new row, and a search without an id always writes
officer.queue_searchedAn officer searched the fallout queue by phone, serial or order id. Recorded on activation for the activations queue and esim_store_order for the store queue. Values carry the queue kind, the term's kind, the number of rows on the returned page and the portal's search id, never the term. Only the first page of a search writes a row, and a re-run of the same search id by the same officer within 10 minutes writes none; later pages and the unfiltered queue are not audited
document.viewedA staff member downloaded someone else's PDF document through GET /documents/:type/:entityId.pdf. Values carry the document type. A repeat download of the same document by the same staff member within 10 minutes writes no new row
security.refusedA request was refused by a permission or separation-of-duty rule (staff scope, company admin role lock, refund self-decision, refund opened by a decider, an officer refunding an activation it has not claimed, a non-customer calling a customer payment route). Values carry the error code. Written even when the refused work rolls back
plugin.email_test_sentA test email was sent from the email plugin settings. Values carry the masked target address
officer.delivery_email_resentAn officer re-sent a customer's eSIM delivery email
officer.operator_checkedAn officer asked the operator directly what became of an in-progress activation. Values carry the lookup used, the operator's raw state, and the outcome
org.settings_updatedThe seller-of-record organization settings were updated
auth.phone_changedA user changed their phone number
auth.signed_inA staff member or company admin signed in. Values carry the method (otp or magic_link). A customer's sign-in writes no row; the sessions table keeps it
auth.sign_in_refusedA sign-in to a known account was refused. Values carry the method and the reason (deactivated, wrong_client, link_used, link_revoked, link_expired, company_not_approved). A wrong, expired or locked-out code is not recorded, since anyone can send one for any account
auth.signed_outA staff member or company admin signed out. A customer's sign-out writes no row
profile.updatedA user changed their own name or email through PUT /profile or PUT /auth/me. Values carry only the names of the changed fields (fields), never the values
user.identity_type_changedA customer's identity type changed. Old and new type in the values, with source set to profile_update or identity_check
PROFILE_DELETION_REQUESTEDA user requested account deletion
RETENTION_SWEEP_EXECUTEDThe PDPL retention sweep ran. The actor is the staff member who ran it
BREACH_REPORT_GENERATEDA breach notification report was generated. The actor is the staff member who generated it

Catalog and store ​

ActionMeaning
package.customization_updatedA package admin override was edited
store.provider_toggledA store provider was enabled or disabled
store.product_customization_updatedA store product override was edited
store.order_requeuedA stuck store order was re-enqueued for fulfillment
store.fx_rates_refreshedThe daily FX rate batch was stored
store.sync_queuedA store catalog sync was queued

Notifications ​

ActionMeaning
NOTIFICATION_TRIGGEREDA staff member sent a notification (resend, invite, batch) and it was accepted into the pipeline. A system-triggered notification writes no row; its counts are in the notification and delivery rows
NOTIFICATION_BATCH_CREATEDAn administrative notification batch was accepted
NOTIFICATION_BATCH_CANCELLEDA notification batch was cancelled before execution

B2B companies ​

ActionMeaning
company.registeredA company was registered
company.approveA company was approved. Every status action (approve through unarchive) carries the previous status in the old values, and the new status and the reason in the new values
company.rejectA company was rejected
company.suspendA company was suspended
company.reactivateA company was reactivated
company.archiveA suspended or rejected company was archived
company.unarchiveAn archived company was returned to the status it was archived from
company.updatedAn internal admin edited a company's details. Old and new values carry only the changed fields, with the contact phone and email masked
company.payment_mode_setThe company's payment mode was chosen or switched. Old values carry the previous mode
company.user.invitedA portal user was invited. Written on the company with the user id and company role; an invite with no company yet is written on the user
company.user.deactivatedA company portal user was deactivated. Values carry the user id and company role
company.user.removedA company portal user was deleted. Values carry the user id and company role
company.payment_document_viewedA staff member downloaded a contract payment document. Values carry the contract and payment ids
company.roster_stageRoster rows were staged from a CSV upload. Values carry the counts and the created roster ids
company.roster_exportThe roster CSV was exported
company.roster_assignA beneficiary's package assignment was changed or cleared. Old values carry the previous assignment
company.roster_removeA beneficiary was removed from the roster
company.links_sendActivation invite links were minted and delivered. Values carry the counts and the minted roster ids, never the tokens
company.links_exportActivation invite links were minted for self-distribution. Values carry the counts and the minted roster ids, never the tokens
company.links_revokeOpen invitations were revoked. Values carry the count and the revoked roster ids
company.quote_submitA quote draft was submitted for review
company.quote_acceptA countered quote version was accepted
company.quote_declineA countered quote version was declined
company.quote_review_startA submitted quote moved into review
company.quote_counterA quote was countered with a new linked version
company.quote_rejectA quote was rejected
company.contract_confirmA quote was accepted and its contract created
company.payment_recordA contract payment was recorded. Values carry the payment id
company.billing_exportThe billing CSV was exported

Internal documentation - Activation System