Structured events emitted by the ZK Payroll contract suite. This is the authoritative schema reference for SDK, indexer, dashboard, and compliance contributors.
- Naming convention:
PascalCaseevent names emitted asSymboltopics. The legacypayrollcontract uses an older two-symbol convention (("payroll", "<verb>")). - Topic/data layout: Every event is a
(topics, data)tuple on Soroban. Topics always carry the event name and primary identifiers so that filters can subscribe without parsing the data payload. - Privacy boundary: No event in this suite emits a raw salary value. Commitments and keyed-commitment hashes are safe to publish; plaintext amounts are NOT.
Tip: Pair this reference with:
- monitoring/event-taxonomy.md ? the canonical category taxonomy (
ONB,FND,EXE,AUD,SEC).- monitoring/event-severity-mappings.md ? severity levels and alert routing guidance.
- payload-examples.md ? sample XDR payloads.
| Concept | Convention |
|---|---|
topics[0] |
Symbol event name (e.g. "CompanyRegistered") |
topics[1..] |
Primary identifiers (company_id, employee, auditor, ?) |
data |
Payload tuple in declaration order |
| Empty data | Serialised as () (Rust unit) |
| Amount fields | i128 raw token units ? divide by token decimals before display |
| Timestamps | u64 Unix seconds from env.ledger().timestamp() |
| Ledger fields | u32 from env.ledger().sequence() |
| Identifiers | company_id is u64 (registry) or Symbol (audit / batch facade ? see notes) |
Topic format ? modern contracts
topics = ( Symbol("<EventName>"), <primary_id_1>, <primary_id_2>, ... )
data = ( <payload_field_1>, <payload_field_2>, ... )
Topic format ? pause_manager (legacy two-symbol convention)
topics = ( Symbol("PauseManager"), Symbol("<verb>") )
data = ( <payload> ) | ()
Topic format ? legacy payroll batch facade (older contract path)
topics = ( Symbol("payroll"), Symbol("<verb>") )
data = ( <payload> )
| Domain | Section | Primary contract(s) |
|---|---|---|
| Employee lifecycle & onboarding | ? Employee Events | payroll_registry, salary_commitment |
| Payroll execution & proof | ? Payroll Events | payment_executor, payroll (legacy) |
| Treasury administration | ? Treasury Events | payroll_registry, pause_manager |
| Audit & compliance | ? Audit Events | audit_module |
| ZK proof verification | ? Proof Events | proof_verifier (cross-contract effects) |
These events track the lifecycle of employees and their commitment state. HR UIs, identity providers, and roster indexers subscribe to them.
Emitted when a new employee and their Poseidon commitment are registered
under a company. Initial status defaults to Active.
topics[0] Symbol("EmployeeAdded")
topics[1] u64 company_id
topics[2] Address employee
data (BytesN<32> commitment,)
| Severity | Consumers |
|---|---|
LOW (routine onboarding) |
HR UIs, indexers, employee directories |
Emitted when an employee record is permanently deleted from the registry.
topics[0] Symbol("EmployeeRemoved")
topics[1] u64 company_id
topics[2] Address employee
data ()
| Severity | Consumers |
|---|---|
MEDIUM (destructive) |
HR UIs, access-review tools, retention-archival pipelines (see retention-archival-policy.md) |
Emitted when a registered employee is marked Inactive via
set_employee_status. Deactivated employees remain in storage but are not
eligible for payroll execution until reactivated.
topics[0] Symbol("EmployeeDeactivated")
topics[1] u64 company_id
topics[2] Address employee
data (EmployeeStatus previous_status, EmployeeStatus new_status, u32 ledger_sequence, u64 timestamp)
| Field | Meaning |
|---|---|
previous_status |
Status observed before the update. |
new_status |
Always EmployeeStatus::Inactive. |
ledger_sequence |
Ledger sequence at which the lifecycle change was recorded. |
timestamp |
Ledger timestamp for ordering history views. |
| Severity | Consumers |
|---|---|
MEDIUM |
HR UIs, payroll eligibility caches, audit-log indexers |
Emitted when a registered employee is marked Active via
set_employee_status. Reactivated employees become eligible for payroll
execution once other client-side checks pass.
topics[0] Symbol("EmployeeReactivated")
topics[1] u64 company_id
topics[2] Address employee
data (EmployeeStatus previous_status, EmployeeStatus new_status, u32 ledger_sequence, u64 timestamp)
| Field | Meaning |
|---|---|
previous_status |
Status observed before the update. |
new_status |
Always EmployeeStatus::Active. |
ledger_sequence |
Ledger sequence at which the lifecycle change was recorded. |
timestamp |
Ledger timestamp for ordering history views. |
| Severity | Consumers |
|---|---|
MEDIUM |
HR UIs, payroll eligibility caches, audit-log indexers |
Emitted when a registered employee is moved to Incomplete. This state is
used when required registration data is missing and should be treated as
ineligible until corrected.
topics[0] Symbol("EmployeeStatusUpdated")
topics[1] u64 company_id
topics[2] Address employee
data (EmployeeStatus previous_status, EmployeeStatus new_status, u32 ledger_sequence, u64 timestamp)
| Severity | Consumers |
|---|---|
LOW |
HR UIs, roster validation jobs |
Emitted when a new commitment is stored (store_commitment) or an existing
one is updated (update_commitment). The same event name is also emitted
from payroll_registry.update_commitment with (company_id, employee) as
the topic tuple ? indexers MUST subscribe by contract address to
disambiguate.
From salary_commitment:
topics[0] Symbol("CommitmentUpdated")
topics[1] Address employee
data (BytesN<32> commitment,)
From payroll_registry:
topics[0] Symbol("CommitmentUpdated")
topics[1] u64 company_id
topics[2] Address employee
data (BytesN<32> new_commitment,)
| Severity | Consumers |
|---|---|
LOW |
Roster caches, salary-history builders, indexers |
?? A
CommitmentUpdatedfollowed by aCommitmentRotatedin the same transaction indicates the old commitment has been invalidated.
Emitted when an existing commitment is explicitly rotated (old value revoked, new value stored). Indexers that cache commitments MUST replace their cached value on receipt of this event.
topics[0] Symbol("CommitmentRotated")
topics[1] Address employee
data (BytesN<32> old_commitment, BytesN<32> new_commitment)
| Severity | Consumers |
|---|---|
MEDIUM (authoritative invalidation signal) |
Indexers, salary-history rebuilders, audit trails |
Emitted when an employee's commitment is locked to prevent silent updates
during a finalized payroll draft or audited payroll run. After this event,
update_commitment and rotate_commitment will panic until unlocked.
topics[0] Symbol("CommitmentLocked")
topics[1] Address employee
data ()
| Severity | Consumers |
|---|---|
MEDIUM (routine during payroll finalisation; treat as integrity signal ? any unaccompanied unlock should be investigated) |
HR UIs, compliance reviewers, monitoring |
Emitted when an employee's commitment lock is cleared by the admin. Should
only follow a previously observed CommitmentLocked for the same employee.
topics[0] Symbol("CommitmentUnlocked")
topics[1] Address employee
data ()
| Severity | Consumers |
|---|---|
MEDIUM |
HR UIs, monitoring (verify it was preceded by an authorised lock) |
Emitted when an external HR-system reference ID (e.g. "EMP12345") is bound
to an employee address. IDs are non-sensitive metadata only ? never include
salary or bank information.
topics[0] Symbol("ReferenceIdSet")
topics[1] Address employee
data (String reference_id,)
| Severity | Consumers |
|---|---|
LOW |
HR-system bridges, identity providers, integration indexers |
Step 1 of the two-step HR-admin rotation. Signals a pending change of the
HR admin address. Must be followed by AdminRotationAccepted (rotates the
admin) or AdminRotationCancelled (reverts).
topics[0] Symbol("AdminRotationProposed")
topics[1] Address current_admin
data (Address new_admin,)
| Severity | Consumers |
|---|---|
MEDIUM |
Security dashboards, pending-change monitors |
Step 2 of the rotation: the proposed admin has accepted and now holds the HR-admin role.
topics[0] Symbol("AdminRotationAccepted")
topics[1] Address new_admin
data ()
| Severity | Consumers |
|---|---|
HIGH (privileged handoff) |
Security dashboards, key-management tools |
The current admin cancelled a pending rotation before acceptance. The admin set did NOT change.
topics[0] Symbol("AdminRotationCancelled")
topics[1] Address current_admin
data ()
| Severity | Consumers |
|---|---|
LOW |
Security dashboards (expected if not accepted within timeout) |
These events cover payroll periods, individual payment execution, and the proof-verified settlement step. Reconciliation tools, payment dashboards, and indexers MUST subscribe to this domain.
Emitted when a new payroll period is opened for a company. Periods are monotonically numbered per company. A new period cannot be opened while a prior one is still open.
topics[0] Symbol("PeriodCreated")
topics[1] u64 company_id
data (u32 period_id,)
| Severity | Consumers |
|---|---|
INFO (expected lifecycle) |
Indexers, payroll schedulers, dashboards |
Emitted when a payroll period is closed. No further payments can be made against the period after this event.
topics[0] Symbol("PeriodClosed")
topics[1] u64 company_id
data (u32 period_id,)
| Severity | Consumers |
|---|---|
INFO (expected lifecycle) |
Indexers, reconciliation tools (use to finalise per-period totals) |
Emitted after a successful private payment execution via the period-aware
payment path. The included amount is a plaintext value (the off-chain
prover has already disclosed it to the contract). Indexers can trust this
event only after the same transaction's proof_verifier.verify succeeded.
topics[0] Symbol("PayrollProcessed")
topics[1] u64 company_id
data (Address employee, i128 amount, u32 period_id)
| Severity | Consumers |
|---|---|
LOW (high-volume success) |
Reconciliation tools, payment dashboards, per-company & per-period aggregators |
?? Distinguish from the legacy
payment_executedevent (? Legacy Batch Events):PayrollProcessedcarriescompany_idintopics[1]andperiod_idindata[2].
The older payroll contract uses a two-symbol topic convention. Treat it
as a separate emission source, but normalise records into a common schema
keyed on (company_id, employee, period, amount, ledger_sequence).
topics[0] Symbol("payroll")
topics[1] Symbol("payment_executed")
data (Address employee, i128 amount)
| Severity | Consumers |
|---|---|
LOW |
Reconciliation tools, legacy dashboards |
topics[0] Symbol("payroll")
topics[1] Symbol("run_executed")
data (u64 run_id, i128 total_amount)
| Severity | Consumers |
|---|---|
LOW |
Run-aggregators (track run_id sequence for gap detection) |
For the cross-call payroll lifecycle, consumers should process the legacy
payroll topics in this order:
run_prepared- a pending run was created.run_approved- an authorized reviewer recorded approval.payment_executed- an individual payment completed.run_executed- the execution batch completed.reconciliation_updatedwithReconciled- settlement completed.
The preparation and execution calls allocate separate run IDs in the current
API. Consumers must use the event payload run ID when correlating records and
must not infer correlation from event position alone. Event-ordering tests
assert topics only; they do not log or export payroll amounts, commitments, or
employee addresses. A failed or unauthorized approval reverts and emits no
run_approved event.
topics[0] Symbol("payroll")
topics[1] Symbol("draft_updated")
data (u64 draft_id, Symbol period_label, i128 total_amount, u32 employee_count, u32 amendment_count)
| Severity | Consumers |
|---|---|
LOW |
SDKs, dashboards, draft-review indexers |
topics[0] Symbol("payroll")
topics[1] Symbol("deposit")
data (Address from, i128 amount)
| Severity | Consumers |
|---|---|
LOW (treasury deposits) |
Funding dashboards, treasury monitors |
?? The legacy
payrollcontract emits additional event types beyond the three enumerated above (for exampledraft_amended, run lifecycle and submission events). This document only covers the high-priority, externally documented topics. To enumerate the full surface, subscribe by contract address and inspect topics at runtime, or readcontracts/payroll/src/lib.rs/libmain.rsdirectly.
Events describing company onboarding and privileged-role administration that affect treasury behavior (admin/treasury rotation, pause control).
Emitted when a new company is registered. Carries the initial admin and treasury addresses used for subsequent payment execution.
topics[0] Symbol("CompanyRegistered")
topics[1] u64 company_id
data (Address admin, Address treasury)
| Severity | Consumers |
|---|---|
MEDIUM (new tenant onboarding) |
Onboarding analytics, tenant indexers, billing |
The two-step rotation pattern is implemented in two contracts and emits the
canonical topic names listed under ? Employee Events for
salary_commitment, and under pause_manager events below for pause_manager. Subscribe by contract address to
distinguish:
| Contract | Event topic | Section |
|---|---|---|
salary_commitment |
AdminRotationProposed / Accepted / Cancelled |
? Employee Events |
pause_manager |
PauseManager / op_proposed / op_rotated / op_cancelled |
? pause_manager events |
??
payroll_registryexposespropose_*_rotation/accept_*_rotation/cancel_*_rotationstorage functions for company-level admin and treasury roles but does not emit rotation events. Off-chain monitors tracking privileged changes at the company level must poll registry storage or rely on companion indexers rather than subscribe to events.
Emitted when the system is paused. All payroll execution halts immediately
until unpaused. Treat as CRITICAL.
topics[0] Symbol("PauseManager")
topics[1] Symbol("paused")
data ()
| Severity | Consumers |
|---|---|
CRITICAL (page on-call) |
All operations, incident-response runbooks (see incident-response-playbook.md) |
Emitted when the system resumes. Operators MUST verify the root cause of the
preceding paused event before treating this as routine.
topics[0] Symbol("PauseManager")
topics[1] Symbol("unpaused")
data ()
| Severity | Consumers |
|---|---|
HIGH |
All operations, incident-response runbooks |
Compliance-grade events from audit_module. They carry only metadata ?
salary values are NEVER emitted, consistent with the privacy boundary.
Emitted when a view key is generated for an auditor. Carries only the
ledger at which the key expires — the raw key bytes are returned directly
to the caller from generate_view_key but are deliberately not
included in the event, since publishing the key material itself in a
public event would let anyone perform keyed-commitment checks without ever
holding a genuine grant.
topics[0] Symbol("ViewKeyGenerated")
topics[1] Address auditor
data (u32 expiration_ledger,)
| Severity | Consumers |
|---|---|
MEDIUM (privileged credential issued) |
Audit dashboards, key-lifecycle trackers, SOC logs |
?? The canonical source only emits
AuditAccessRevoked(see below). The nameViewKeyRevokeddoes not appear in the currentmasteremission surface ? it is documented here only so that older indexers / external tooling can recognise it if they encounter it from legacy deployments. TreatAuditAccessRevokedas the authoritative event.
Emitted when a view key is revoked before its expiration. The revoking admin and the affected auditor are both carried as topics; there is no data payload (only emitted on success — a rejected revocation attempt, e.g. wrong granter or unknown auditor, emits no event at all, so this event is always a reliable, non-ambiguous "access is now cut off" signal).
topics[0] Symbol("AuditAccessRevoked")
topics[1] Address admin // the admin that originally granted the key
topics[2] Address auditor // the auditor losing access
data () // no payload
| Severity | Consumers |
|---|---|
HIGH (privileged credential destroyed) |
Compliance reviewers, security dashboards, audit trails |
Emitted when an auditor's commitment verification passes. The second data
field is a keyed commitment (SHA-256(view_key ? commitment)) ? not the
raw salary commitment, so it is safe to publish for off-chain auditing.
topics[0] Symbol("AuditSuccessful")
topics[1] Address auditor
data (AuditScope scope, BytesN<32> keyed_stored)
AuditScope serialises as a u32 discriminant:
| Value | Scope |
|---|---|
0 |
FullCompany |
1 |
TimeRange |
2 |
EmployeeList |
3 |
AggregateOnly |
| Severity | Consumers |
|---|---|
INFO (routine compliance check) |
Compliance dashboards, audit-trace builders |
Emitted when an aggregate compliance report is generated for a company and period. No individual salary data is included.
topics[0] Symbol("AggregateAuditGenerated")
topics[1] Address auditor
data (Symbol company_id, u64 period_start, u64 period_end)
??
company_idhere is the audit-moduleSymbolform (string-coerced identifier), which differs from theu64integer ID used bypayroll_registryandpayment_executor.
| Severity | Consumers |
|---|---|
INFO |
Compliance dashboards, period-aggregators |
Emitted when an exportable metadata summary is produced for external compliance tooling (issue #93). The summary includes only verification counts and metadata ? never salary values.
topics[0] Symbol("AuditSummaryExported")
topics[1] Address auditor
data (Symbol company_id, u64 period_start, u64 period_end, u32 total_entries)
| Severity | Consumers |
|---|---|
LOW |
External compliance integrations, evidence-collectors |
proof_verifier is a storage-only contract; it does not emit events directly.
However, payment_executor.execute_payment integrates proof verification
into the settlement path, and the success or failure of a payment is the
public signal that a Groth16 proof was or was not verified.
A failed proof verification causes a Soroban transaction reversion (
"Invalid payment proof"); it does NOT emit an explicit event. Off-chain monitors must listen for failed transactions and read the diagnostic string from the host envelope.
Every PayrollProcessed event (? Payroll Events) is also an implicit
proof-verification success: the contract only emits it after
proof_verifier.verify returned true and the asset allowlist passed.
Cross-domain signal contract:
| Proof result | On-chain signal | Consumer guidance |
|---|---|---|
| Success | PayrollProcessed event in same tx |
Indexers may treat as authoritative proof-acceptance |
Failure (CommitmentMismatch, malformed proof, etc.) |
Transaction reverts with host diagnostic | Detect via failed-tx monitoring; see event-severity-mappings.md Exceptional Conditions |
Stale proof (ProofExpired, > 7 days) |
PaymentError::ProofExpired reversion |
Detect via failed-tx monitoring; ? Operational Impact |
| Replay attempt | PaymentError::ProofAlreadyUsed reversion |
CRITICAL alert (see alert-rules.md ? 2) |
| Payment in closed period | PaymentError::PeriodClosed reversion |
MEDIUM alert (see alert-rules.md ? 6) |
| Severity | Consumers |
|---|---|
See PayrollProcessed for success |
See PayrollProcessed |
Quick-reference: which consumer types should subscribe to which domain.
| Consumer | Employee | Payroll | Treasury | Audit | Proof |
|---|---|---|---|---|---|
| HR/Employee UI | ? | ? | ? | ? | ? |
| Payroll operator UI | ? | ? | ? | ? | ? |
| Payment indexer | ? | ? | ? | ? | ? |
| Reconciliation tool | ? | ? | ? | ? | ? |
| Treasury dashboard | ? | ? | ? | ? | ? |
| Compliance dashboard | ? | ? | ? | ? | ? |
| Audit-trace builder | ? | ? | ? | ? | ? |
| Incident-response alerts | ? (locks) | ? | ? (pause) | ? (revokes) | ? (failed-tx) |
| External compliance export | ? | ? | ? | ? | ? |
| Identity provider / HR-system bridge | ? (ReferenceIdSet) |
? | ? | ? | ? |
- Subscribe by contract address and topic discriminant ? multiple
contracts emit events that overlap by name (e.g.
CommitmentUpdatedis emitted by bothsalary_commitmentandpayroll_registry). - Decode topics and data using
ScVal(Symbol,Address,u32,u64,i128,BytesN<N>, tuples). The Stellar SDK'sEventFilteris the recommended subscription mechanism. i128amount fields are raw token units ? divide by the token's decimal precision before storing or displaying.- The two payment paths (
payment_executorand legacypayroll) use different topic layouts ? normalise records to a common schema keyed on(company_id, employee, period, amount, ledger_sequence)to avoid double-counting. company_idis au64integer inpayroll_registry/payment_executor, but aSymbolstring identifier inaudit_module. Convert both to a stable off-chain UUID namespace when correlating across domains.- Failed proof verifications do NOT emit an event ? monitor host-level transaction failures and parse reversion diagnostics instead.
- This document covers the v0 event surface emitted by
master. Older deployments may emit the legacypayment_executedandViewKeyRevokednames ? see interop/proof-schema-version-negotiation.md for the negotiation protocol used when older and newer clients coexist. - New events MUST be added here AND in event-taxonomy.md AND event-severity-mappings.md before a release ships.
- Renaming an event is a breaking change ? use topic aliases during deprecation windows, and document the migration window in release notes.
| Event | Contract | topics |
|---|---|---|
EmployeeRemoved |
payroll_registry |
("EmployeeRemoved", u64 company_id, Address employee) |
CommitmentLocked |
salary_commitment |
("CommitmentLocked", Address employee) |
CommitmentUnlocked |
salary_commitment |
("CommitmentUnlocked", Address employee) |
AdminRotationAccepted |
salary_commitment |
("AdminRotationAccepted", Address new_admin) |
AdminRotationCancelled |
salary_commitment |
("AdminRotationCancelled", Address current_admin) |
PauseManager / paused |
pause_manager |
("PauseManager", "paused") |
PauseManager / unpaused |
pause_manager |
("PauseManager", "unpaused") |
| Event | Contract | topics | data |
|---|---|---|---|
EmployeeAdded |
payroll_registry |
(company_id, employee) |
(commitment,) |
CommitmentUpdated |
salary_commitment |
(employee) |
(commitment,) |
CommitmentUpdated |
payroll_registry |
(company_id, employee) |
(new_commitment,) |
CommitmentRotated |
salary_commitment |
(employee) |
(old, new) |
ReferenceIdSet |
salary_commitment |
(employee) |
(reference_id,) |
AdminRotationProposed |
salary_commitment |
(current_admin) |
(new_admin,) |
CompanyRegistered |
payroll_registry |
(company_id) |
(admin, treasury) |
PeriodCreated |
payment_executor |
(company_id) |
(period_id,) |
PeriodClosed |
payment_executor |
(company_id) |
(period_id,) |
PayrollProcessed |
payment_executor |
(company_id) |
(employee, amount, period_id) |
ViewKeyGenerated |
audit_module |
(auditor) |
(expiration_ledger,) |
AuditAccessRevoked |
audit_module |
(admin, auditor) |
() |
AuditSuccessful |
audit_module |
(auditor) |
(scope, keyed_stored) |
AggregateAuditGenerated |
audit_module |
(auditor) |
(company_id, period_start, period_end) |
AuditSummaryExported |
audit_module |
(auditor) |
(company_id, period_start, period_end, total) |
PauseManager / op_proposed |
pause_manager |
("PauseManager", "op_proposed") |
(current_operator, new_operator) |
PauseManager / op_rotated |
pause_manager |
("PauseManager", "op_rotated") |
Address new_operator (bare Address ? single-value data) |
PauseManager / op_cancelled |
pause_manager |
("PauseManager", "op_cancelled") |
Address current_operator (bare Address ? single-value data) |
| Reference | Path |
|---|---|
| Event taxonomy (canonical categories) | docs/monitoring/event-taxonomy.md |
| Event severity mappings | docs/monitoring/event-severity-mappings.md |
| Alert rules | docs/monitoring/alert-rules.md |
| Payload examples | docs/payload-examples.md |
| SLA operational targets | docs/SLA_OPERATIONAL_TARGETS.md |
| Health metrics | docs/monitoring/health-metrics-observability.md |
payroll_registry contract |
contracts/payroll_registry/src/lib.rs |
salary_commitment contract |
contracts/salary_commitment/src/lib.rs |
payment_executor contract |
contracts/payment_executor/src/lib.rs |
audit_module contract |
contracts/audit_module/src/lib.rs |
pause_manager contract |
contracts/pause_manager/src/lib.rs |
proof_verifier contract |
contracts/proof_verifier/src/lib.rs |
Legacy payroll contract |
contracts/payroll/src/lib.rs |