Events
The eleven webhook event types, the envelope every event uses, and what each payload contains.
A
JEvery webhook Cavuno sends uses the same JSON envelope. What changes between events is the type field and the object inside data.
Event types
| Event | Fires when |
|---|---|
job.created | A job is added to your board. |
job.updated | A job changes in a way visible to operators. |
job.deleted | A job is removed. |
company.created | A company is added to your board. |
company.updated | An allowlisted company field changes. |
company.deleted | A company is removed. |
candidate.created | A board user becomes a candidate you can see. |
candidate.updated | A candidate's email address or display name changes. |
candidate.deleted | A candidate's data has been permanently erased. |
marketing_permission.granted | Someone agreed to receive marketing email from you. |
marketing_permission.withdrawn | Someone no longer consents, or their account was deleted. |
There is no job.published or job.expired event. Publishing, pausing, and expiring a job all arrive as job.updated. Read status in the snapshot and changed_fields in the payload to see what happened.
Two other details are worth knowing. candidate.deleted fires only once erasure has finished, not when a deletion is requested. Re-saving a record without changing anything produces no event at all.
The two marketing events describe consent, which lives on the board user record — both candidates and employers can hold it. Wire them into your newsletter tool: add the person on granted, remove them on withdrawn. A withdrawal carries a reason; account_deleted means the person's account is gone, so delete them from your own systems too. Someone who never decided produces no event and appears in no list — absence means no consent, never a default.
The event envelope
123456789101112131415161718192021222324252627{"id": "evt_01EXAMPLEJOBUPDATED","object": "event","type": "job.updated","schema_version": "1","occurred_at": "2026-07-24T13:00:00Z","board_id": "acc_01EXAMPLEBOARD","data": {"object": {"id": "jobs_01EXAMPLEJOB","object": "job","slug": "senior-engineer","title": "Staff Engineer","status": "published","company_id": "companies_01EXAMPLE","company_name": "Acme","location": "Remote","employment_type": "full_time","workplace_type": "remote","published_at": "2026-07-24T12:00:00Z","expires_at": "2026-08-23T12:00:00Z","updated_at": "2026-07-24T13:00:00Z","revision": 2},"changed_fields": ["title"]}}
Use id to recognise an event you have already handled. It stays the same across retries and replays. Use revision to reject an older event that arrives after a newer one. occurred_at tells you when the change happened, but it is not an ordering guarantee.
What each snapshot contains
Payloads are built from a fixed list of fields. Nothing else is included, even if the record holds more.
| Object | Fields |
|---|---|
| Job | id, object, slug, title, status, company_id, company_name, location, employment_type, workplace_type, published_at, expires_at, updated_at, revision |
| Company | id, object, slug, name, website, logo_url, updated_at, revision |
| Candidate | id, object, email, display_name, created_at, updated_at, revision |
| Marketing permission | id, object, email, display_name, role, status, source, reason, granted_at, withdrawn_at, updated_at, revision |
Companies carry no location field, because the company record has no canonical public location. logo_url can be null for older logos.
On a marketing permission, id is the board user's id, source names the surface where the decision was made (for example candidate_sign_up or notification_preferences), and reason is set only on withdrawals: person_request, operator_request, or account_deleted. The wording the person agreed to is not included — it belongs to your board's pages, and the record stores the decision, not the prose.
Changed fields
On an update, changed_fields lists which of a fixed set of fields changed. It is always empty on create and delete events.
| Object | Fields that can appear |
|---|---|
| Job | slug, title, status, company_id, company_name, location, employment_type, workplace_type, published_at, expires_at |
| Company | slug, name, website, logo_url |
| Candidate | email, display_name |
| Marketing permission | status, source, reason, granted_at, withdrawn_at |
A change to a field outside this list does not appear in changed_fields, and for candidates it does not produce an event. Editing a candidate's bio, for example, changes nothing on the wire.
Deleted events
A delete event carries a tombstone instead of a snapshot. It holds only what you need to remove your own copy.
1234567891011121314151617181920{"id": "evt_01EXAMPLEJOBDELETED","object": "event","type": "job.deleted","schema_version": "1","occurred_at": "2026-07-24T14:00:00Z","board_id": "acc_01EXAMPLEBOARD","data": {"object": {"id": "jobs_01EXAMPLEJOB","object": "job","revision": 3,"deleted": true,"deleted_at": "2026-07-24T14:00:00Z","slug": "senior-engineer","company_id": "companies_01EXAMPLE"},"changed_fields": []}}
Treat the tombstone as final. The object no longer exists, so reading it back from the API will not work.
Schema versions
Every event carries schema_version. Fields may be added to an existing version, so ignore any field you do not recognise. A change that would break existing receivers ships as a new version instead.
Next steps
Check the signature on every request before you act on it. See Verifying signatures.