Events
The thirteen 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. |
digital_product.purchased | Someone bought or claimed one of your digital products. |
digital_product.refunded | The refunded total on a digital product sale changed: a refund, or a refund that failed. |
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 digital product events carry the purchase: the product's id, slug and name, the buyer's email, the amount and the refunded total. Reading the digital product events needs the billing read permission, like transactions. Each partial refund sends digital_product.refunded with the new total in refunded_amount_cents. If a refund then fails or is canceled at the bank, the event is sent again with the lowered total, so treat refunded_amount_cents as the current total rather than adding amounts up; a retried refund is announced again. A refund does not by itself remove the buyer's access, so read status too. List a product's buyers with GET /v1/plans/{id}/purchases.
The event envelope
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.
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.