worker.contract.updated Event

Emitted when a worker's contract changes in any of these ways:

  • status (e.g. DRAFTPENDING_SIGNATUREEXECUTEDTERMINATED)
  • jobTitle
  • managerId

Other field changes on the contract do not emit this event.

PropertyValue
Typeworker.contract.updated
Data version1
Entityentity.type = "workerContract", entity.id = data.id
Fan-outOne delivery per matching subscription
Mutual exclusionSuppressed 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

A single contract snapshot mirroring the public API's WorkerContract schema, plus two routing fields (workerId, and a links.self pointing at the parent worker).

FieldTypeDescription
idstringPublic contract ID (lglsoctc_…). Same value as entity.id.
clientIdstringPublic client ID this contract is on.
workerIdstringPublic worker ID this contract belongs to (lglsowpr_…).
namestringContract display name at the moment of emit.
statusstringE.g. DRAFT, PENDING_SIGNATURE, EXECUTED, TERMINATED. Snapshot at emit time.
billingFrequencystringE.g. MONTHLY, BIWEEKLY.
isoCountryCodestringCountry of work.
jobTitlestringJob title at the moment of emit.
startDateISO-8601 dateContract start.
contractEndDatestring | nullISO-8601 date or null for open-ended.
createdAtISO-8601 UTCWhen the contract record was first created in Listo. Immutable.
engagementTypestringE.g. EOR, CONTRACTOR.
managerIdstring | nullPublic user ID of the manager, or null.
links.selfstringPath to the parent worker (no per-contract public endpoint exists yet).

🛈 No per-contract public endpoint. links.self points at the parent worker — fetch the full worker, find the matching contract by id in the worker's contracts[]. A dedicated contract endpoint may be added later; the link will swap to that without bumping dataVersion.


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.updated for those.
  • No "previous" values. This is a forward snapshot, not an audit-log entry.

Mutual exclusion with worker.onboarded

The 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.updated events for the same contract may arrive out of occurredAt order. Order by occurredAt if your downstream needs sequence accuracy.
  • A worker.contract.updated may arrive before the worker.onboarded for 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.