worker.contract.updated Event
Emitted when a worker's contract changes in any of these ways:
status(e.g.DRAFT→PENDING_SIGNATURE→EXECUTED→TERMINATED)jobTitlemanagerId
Other field changes on the contract do not emit this event.
| Property | Value |
|---|---|
| Type | worker.contract.updated |
| Data version | 1 |
| Entity | entity.type = "workerContract", entity.id = data.id |
| Fan-out | One delivery per matching subscription |
| Mutual exclusion | Suppressed for the EXECUTED transition that triggers worker.onboarded. You receive that. |
Sample delivery
POST /your/webhook/path HTTP/1.1
Host: hooks.your-domain.com
Content-Type: application/json
User-Agent: ListoGlobal-Gateway/1.0
webhook-id: lglsoevt_uZN4sQTuCK8NbXgE3
webhook-timestamp: 1746358200
webhook-signature: v1,Y9pXkR2nA5bMqWHj7zEFvLBcYIDsoCLeGJxQXM3yTbY=
{
"id": "lglsoevt_uZN4sQTuCK8NbXgE3",
"type": "worker.contract.updated",
"specVersion": 1,
"dataVersion": 1,
"occurredAt": "2026-05-04T12:10:00.118Z",
"entity": { "type": "workerContract", "id": "lglsoctc_uXYZ1aBcD2EfG3hJ4" },
"data": {
"id": "lglsoctc_uXYZ1aBcD2EfG3hJ4",
"clientId": "lglsocli_uZIIHfKqYBwyaRGGs",
"workerId": "lglsowpr_uZIIQiu8Q5zhUXgbR",
"name": "Alex Kim — Staff Engineer",
"status": "EXECUTED",
"billingFrequency": "MONTHLY",
"isoCountryCode": "US",
"jobTitle": "Staff Engineer",
"startDate": "2026-05-02",
"contractEndDate": null,
"createdAt": "2026-04-28T17:09:11.000Z",
"engagementType": "EOR",
"managerId": "lglsousr_uXYZxLtq9ABvCdEf2",
"links": {
"self": "/v2/instance-integrations/inst_abc123xyz/clients/lglsocli_uZIIHfKqYBwyaRGGs/workers/lglsowpr_uZIIQiu8Q5zhUXgbR"
}
}
}data schema
data schemaA single contract snapshot mirroring the public API's WorkerContract
schema, plus two routing fields (workerId, and a links.self pointing
at the parent worker).
| Field | Type | Description |
|---|---|---|
id | string | Public contract ID (lglsoctc_…). Same value as entity.id. |
clientId | string | Public client ID this contract is on. |
workerId | string | Public worker ID this contract belongs to (lglsowpr_…). |
name | string | Contract display name at the moment of emit. |
status | string | E.g. DRAFT, PENDING_SIGNATURE, EXECUTED, TERMINATED. Snapshot at emit time. |
billingFrequency | string | E.g. MONTHLY, BIWEEKLY. |
isoCountryCode | string | Country of work. |
jobTitle | string | Job title at the moment of emit. |
startDate | ISO-8601 date | Contract start. |
contractEndDate | string | null | ISO-8601 date or null for open-ended. |
createdAt | ISO-8601 UTC | When the contract record was first created in Listo. Immutable. |
engagementType | string | E.g. EOR, CONTRACTOR. |
managerId | string | null | Public user ID of the manager, or null. |
links.self | string | Path to the parent worker (no per-contract public endpoint exists yet). |
🛈 No per-contract public endpoint.
links.selfpoints at the parent worker — fetch the full worker, find the matching contract byidin the worker'scontracts[]. A dedicated contract endpoint may be added later; the link will swap to that without bumpingdataVersion.
What's not included
- No
changedFields. Diff against your stored copy if you care which field moved. - No worker profile fields (name, email, etc.) — listen for
worker.updatedfor those. - No "previous" values. This is a forward snapshot, not an audit-log entry.
Mutual exclusion with worker.onboarded
worker.onboardedThe first contract for a (worker, client) pair to reach EXECUTED
triggers worker.onboarded. For that
specific status transition Listo suppresses the
worker.contract.updated that would otherwise fire — there is exactly
one event for that moment, and it is worker.onboarded.
For every other change to a contract — later status changes, job-title
edits, manager re-assignments, EXECUTED transitions on subsequent
contracts under the same worker-client pair — worker.contract.updated
fires as documented above.
The mutual exclusion is enforced at the producer; you do not need to deduplicate between the two event types yourself.
Ordering and duplication
- Duplicates are normal. Dedupe on
webhook-id. - Order is not guaranteed. Two
worker.contract.updatedevents for the same contract may arrive out ofoccurredAtorder. Order byoccurredAtif your downstream needs sequence accuracy. - A
worker.contract.updatedmay arrive before theworker.onboardedfor the same worker if the events were produced close together — handle gracefully (upsert by(clientId, workerId, contractId), don't insist on prior existence).
Common patterns
"Track contract status changes"
1. Verify signature, dedupe on webhook-id.
2. UPSERT INTO worker_contracts (id, ...)
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
job_title = EXCLUDED.job_title,
manager_id = EXCLUDED.manager_id,
last_event_at = EXCLUDED.last_event_at
WHERE worker_contracts.last_event_at < EXCLUDED.last_event_at;
3. If new status is TERMINATED, fire your downstream offboarding flow.
4. Respond 200.The last_event_at guard prevents a stale retry from overwriting newer
state.
"Notify a Slack channel on contract status change"
A common shape: filter data.status against a list of statuses you care
about and post to Slack. Make sure to dedupe on webhook-id first — at
least-once delivery means duplicate Slack messages otherwise.
"Use it as a manager-change feed"
managerId lives on the contract, not the worker — manager re-assignments
flow through this event, not worker.updated. Hook this if you need
real-time updates of "who reports to whom" for org-chart automation.
Updated 1 day ago
