Event catalogue
Every webhook event Agoo sends, when it fires and what data.object contains, with an example of each.
This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.
Each event below is delivered to the webhook endpoints subscribed to its type, and is also available from GET/events for 30 days. Every event has the same envelope; data.object is a snapshot of the object, in the same shape as its own endpoint returns.
Examples are trimmed
To keep this page readable, the examples show only the fields of data.object that matter for each event. Real payloads always carry the
complete object. Each event's page in the API reference shows the full schema and a full example.
New event types can be added at any time. Subscribe only to the types you handle, and ignore types you don't recognise. See versioning.
All events
| Event | Fires when | data.object |
|---|---|---|
visit.created | A visit is created: an invitation or pre-registration (from the API, the console or the Workspace app), a walk-in at the kiosk, or a confirmed in-person booking. | Visit |
visit.updated | A visit's details change: site, host, type, times, purpose or custom fields. | Visit |
visit.approval_requested | A visit needs a host's decision and Agoo has asked the host (push, WhatsApp, an SMS link or email). | Visit |
visit.approved | A host, or someone acting for them, approves a visit. | Visit |
visit.denied | A host, or someone acting for them, denies a visit that was awaiting approval. | Visit |
visit.checked_in | A visitor is checked in: at the kiosk, by reception or a guard, after an approval, or through the API. | Visit |
visit.checked_out | A visitor is checked out: at the kiosk, by scanning their pass again, by their host or reception, from the check-out link, automatically at the end of the day, or through the API. | Visit |
visit.cancelled | An expected visit, or one awaiting approval, is cancelled. | Visit |
visit.no_show | An expected visit's day ends, in the site's time zone, without the visitor checking in. | Visit |
visit.needs_someone | A visitor is waiting and nobody has taken the visit: a walk-in for a team, for no one in particular, or for someone whose visitors go through their office. | Visit |
visit.host_changed | The people a visit is for change: a host is added or removed, someone else becomes the lead, or someone takes a visit that was waiting. | Visit |
pass.sent | Agoo sends a visitor their QR pass: when a visit is created with send_pass, when you resend it, or when a booking is rescheduled. | Visit |
delivery.received | A parcel is logged for a person, a team or the front desk: by reception, a guard, a courier at a kiosk or from a drop-off poster, or your system through the API. | Delivery |
delivery.collected | A parcel is collected from reception. | Delivery |
delivery.updated | Reception corrects a waiting parcel: who it's for (a new recipient gets a new collection code), where it's kept or what it is. | Delivery |
delivery.returned | A waiting parcel is sent back to the sender (status returned) or turned away at the door (refused). | Delivery |
booking.created | Someone books a slot, on a booking page, in an embed or through the API. Fires for every booking; status is pending or confirmed. | Booking |
booking.confirmed | The host confirms a pending booking. | Booking |
booking.declined | The host declines a pending booking. | Booking |
booking.expired | A pending booking isn't confirmed in time. | Booking |
booking.rescheduled | A confirmed booking moves to a new time, by the attendee's reschedule link, the host or the API. | Booking |
booking.cancelled | A booking is cancelled, by the attendee's cancel link (also while it's pending), the host or the API. | Booking |
attendance.clocked_in | An employee clocks in, by any method, or a manager adds a clock-in they missed. | Attendance event |
attendance.clocked_out | An employee clocks out, by any method, or a manager adds a clock-out they missed. | Attendance event |
attendance.late | A clock-in is later than the person's shift start plus its grace period. | Attendance event |
attendance.voided | A punch stops counting: a manager voided it, rejected it on review, or corrected its time. | Attendance event |
attendance.absent | Someone's rostered shift ended with no clock-in, no approved leave and no public holiday. | Attendance absence |
attendance.break_started | Someone starts a break, or the paid nursing hour, where break punches are on. | Attendance event |
attendance.break_ended | Someone ends a break, where break punches are on. | Attendance event |
leave.requested | Someone asks for leave that needs approval, in the Workspace app. Leave approved as soon as it's asked for fires leave.approved instead. | Leave request |
leave.approved | Leave is approved: the last approval step is decided, HR or your system records it, or it needs no approval. | Leave request |
leave.declined | An approver declines a leave request. | Leave request |
leave.cancelled | A waiting request, or approved leave that hasn't started, is cancelled by the person, HR or your system. | Leave request |
timesheet.submitted | A timesheet goes for approval after its period: confirmed by the person, sent without their confirmation, or sent back to the first step or reopened (a new round). | Timesheet |
timesheet.approved | A timesheet's last approval step is decided, your system approves it, or it locks itself with no approval steps. It is locked from then on. | Timesheet |
timesheet.reopened | An approved timesheet is reopened, with a reason, in the Console or by your system. | Timesheet |
payroll_export.created | A payroll file is made for a timesheet period, in the Console or with POST /payroll-exports (Pro and above). | Payroll export |
review.published | A review of your page on Agoo Reviews appears, after Agoo's checks (Reviews Business). | |
review.updated | The reviewer changes a published review. | |
review.removed | A published review comes down: removed by a moderator, held, or withdrawn by its author. | |
review.replied | Your reply to a review is published, or a published reply is changed. | |
roster.published | A site's roster is published, in the Console or with POST /rosters/publish. | Roster publication |
shift_swap.requested | A colleague accepts a shift someone offered, and the swap needs approval. | Shift swap |
shift_swap.approved | A swap is approved, or needed no approval: the roster has changed. | Shift swap |
shift_swap.declined | An approver declines a swap. | Shift swap |
shift_claim.requested | Someone claims an open shift and the claim needs approval. | Shift claim |
shift_claim.approved | A claim is approved, or was the first claim where the first one takes the shift. | Shift claim |
shift_claim.declined | An approver declines a claim for an open shift. | Shift claim |
watchlist.matched | Someone checking in matches a watchlist entry. | Watchlist match |
roll_call.started | A roll call starts at a site, from the Workspace app, the console or the API. | Roll call |
roll_call.updated | Someone is accounted for (by their "I'm safe" link or by a marshal), so the counts change. | Roll call |
roll_call.closed | A roll call is closed. | Roll call |
department.created | A department is made, in the Console or through the API. | Department |
department.updated | A department is renamed, moved, given a new head, code or cost centre, archived or brought back. | Department |
position.created | A position is made, in the Console or through the API (Growth and above). | Position |
position.updated | A position's details change, or it's frozen, closed or reopened. | Position |
person.created | A host or employee is added: in the console, by CSV import, by directory sync or through the API. | Person |
person.updated | A person's details change. | Person |
person.deactivated | A person is deactivated. | Person |
person.reactivated | A deactivated person is reactivated in the console. | Person |
person.job_changed | A change to someone's job takes effect: at once, or on its date. | Job change |
hr_document.expiring | A standard HR document reaches one of its type's reminder days before it expires, or the day itself. | HR document |
visitor.erased | A visitor's personal data is erased, through the API, the console or a data subject request. | Visitor erasure |
Visits
visit.created
A visit is created: an invitation or pre-registration (from the API, the console or the Workspace app), a walk-in at the kiosk, or a confirmed in-person booking. data.object.source tells you which.
data.object: Visit · Full schema: visit.created
{
"id": "evt_01m4k5z5h8f8dbawht614vgwsk",
"type": "visit.created",
"created_at": "2026-10-10T15:12:41Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "expected",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"expected_at": "2026-10-14T09:30:00Z",
"expected_until": "2026-10-14T11:30:00Z"
}
}
}visit.updated
A visit's details change: site, host, type, times, purpose or custom fields. Status changes fire their own events instead.
data.object: Visit · Full schema: visit.updated
{
"id": "evt_01m4v10qr8f6kavnzk4fdkdaqb",
"type": "visit.updated",
"created_at": "2026-10-13T16:20:05Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "expected",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"expected_at": "2026-10-14T10:00:00Z",
"expected_until": "2026-10-14T12:00:00Z",
"custom_fields": {
"vehicle_plate": "GR 4521-24"
}
}
}
}visit.approval_requested
A visit needs a host's decision and Agoo has asked the host (push, WhatsApp, an SMS link or email). The visit is awaiting_approval.
data.object: Visit · Full schema: visit.approval_requested
{
"id": "evt_01m4wvzsg0ex6rfc40bsp210yj",
"type": "visit.approval_requested",
"created_at": "2026-10-14T09:30:40Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "awaiting_approval",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"gate_id": "gate_01kjpt7m20fpf9vcsaexsxgqr3",
"expected_at": "2026-10-14T09:30:00Z"
}
}
}visit.approved
A host, or someone acting for them, approves a visit. If the visitor is on site, visit.checked_in follows straight away. If they haven't arrived yet (a pre-registration), the visit is expected again and pass.sent follows once their pass is sent.
data.object: Visit · Full schema: visit.approved
{
"id": "evt_01m4ww0ezgfezvnd1htbzhtss0",
"type": "visit.approved",
"created_at": "2026-10-14T09:31:02Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "checked_in",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"checked_in_at": "2026-10-14T09:31:02Z",
"decision": {
"outcome": "approved",
"decided_at": "2026-10-14T09:31:02Z",
"decided_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"reason": null
}
}
}
}visit.denied
A host, or someone acting for them, denies a visit that was awaiting approval.
data.object: Visit · Full schema: visit.denied
{
"id": "evt_01m4wq1d4geb5bbj7e01cdme22",
"type": "visit.denied",
"created_at": "2026-10-14T08:04:10Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4wpxe60fp5s0pgj6jct2rj6",
"status": "denied",
"type": "other",
"source": "walk_in",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"decision": {
"outcome": "denied",
"decided_at": "2026-10-14T08:04:10Z",
"decided_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"reason": "Not expecting this visitor today."
}
}
}
}visit.checked_in
A visitor is checked in: at the kiosk, by reception or a guard, after an approval, or through the API.
data.object: Visit · Full schema: visit.checked_in
{
"id": "evt_01m4ww0ezgf2bryg9gxb2gbewx",
"type": "visit.checked_in",
"created_at": "2026-10-14T09:31:02Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "checked_in",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"gate_id": "gate_01kjpt7m20fpf9vcsaexsxgqr3",
"checked_in_at": "2026-10-14T09:31:02Z"
}
}
}visit.checked_out
A visitor is checked out: at the kiosk, by scanning their pass again, by their host or reception, from the check-out link, automatically at the end of the day, or through the API.
data.object: Visit · Full schema: visit.checked_out
{
"id": "evt_01m4x1sks8fapr4jzmbnx9c9p1",
"type": "visit.checked_out",
"created_at": "2026-10-14T11:12:09Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "checked_out",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"checked_in_at": "2026-10-14T09:31:02Z",
"checked_out_at": "2026-10-14T11:12:09Z"
}
}
}visit.cancelled
An expected visit, or one awaiting approval, is cancelled. Its pass stops working. Cancelling an in-person booking also cancels its visit.
data.object: Visit · Full schema: visit.cancelled
{
"id": "evt_01m4v9a2z0eb0sxhk2web9nv2d",
"type": "visit.cancelled",
"created_at": "2026-10-13T18:45:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4qne6m0fgjbj3tkcv9xjn5p",
"status": "cancelled",
"type": "meeting",
"source": "invitation",
"site_id": "site_01knvdere0f738bjjhnyhtyv2z",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"expected_at": "2026-10-15T13:00:00Z",
"cancelled_at": "2026-10-13T18:45:00Z"
}
}
}visit.no_show
An expected visit's day ends, in the site's time zone, without the visitor checking in. Agoo marks it no_show.
data.object: Visit · Full schema: visit.no_show
{
"id": "evt_01m4ydnrd0fa0r7aers4df69hb",
"type": "visit.no_show",
"created_at": "2026-10-14T23:59:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4ndb2g0fezspg90a0a470ft",
"status": "no_show",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"expected_at": "2026-10-14T14:00:00Z"
}
}
}visit.needs_someone
A visitor is waiting and nobody has taken the visit: a walk-in for a team, for no one in particular, or for someone whose visitors go through their office. data.object.needs_someone is true and host_id is null; whoever receives the visitor has been told. visit.host_changed follows when someone takes it.
data.object: Visit · Full schema: visit.needs_someone
{
"id": "evt_01m4ydnrd0fa0r7aers4df69hb",
"type": "visit.needs_someone",
"created_at": "2026-10-14T09:12:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4ndb2g0fezspg90a0a470ft",
"status": "awaiting_approval",
"type": "other",
"source": "walk_in",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": null,
"audience": "team",
"hosts": [],
"team": { "id": "team_01m4r6y616e1b973vj50h0643y", "name": "Finance" },
"asked_for": null,
"needs_someone": true,
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
}
}
}
}visit.host_changed
The people a visit is for change: a host is added or removed, someone else becomes the lead host, or someone takes a visit that was waiting. data.object.hosts and host_id are as they are now.
data.object: Visit · Full schema: visit.host_changed
{
"id": "evt_01m4ydnrd0fa0r7aers4df69hb",
"type": "visit.host_changed",
"created_at": "2026-10-13T15:20:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4ndb2g0fezspg90a0a470ft",
"status": "expected",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"audience": "people",
"hosts": [
{ "id": "person_01kjsesxm0e4zbyvkr7bcfxbfg", "name": "Kwame Mensah", "role": "lead", "state": "addressed" },
{ "id": "person_01kjsf2a40ehnsm9gkx4c1z0qp", "name": "Esi Darko", "role": "host", "state": "addressed" }
],
"team": null,
"needs_someone": false,
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
}
}
}
}Passes
pass.sent
Agoo sends a visitor their QR pass: when a visit is created with send_pass, when you resend it, or when a booking is rescheduled. data.object is the visit; pass.channels lists where the pass went.
data.object: Visit · Full schema: pass.sent
{
"id": "evt_01m4k5z8f0e1eanbs211hxv8qs",
"type": "pass.sent",
"created_at": "2026-10-10T15:12:44Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "expected",
"type": "meeting",
"source": "invitation",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"visitor": {
"id": "visitor_01krdtb870fxj8t6ntdtcxwdk5",
"name": "Ama Owusu"
},
"expected_at": "2026-10-14T09:30:00Z",
"pass": {
"channels": ["whatsapp"],
"last_sent_at": "2026-10-10T15:12:44Z"
}
}
}
}Deliveries
A delivery is a parcel, envelope, food or something else received at a site, for a person, a team or the front_desk (recipient_type). Its status is received, then notified once the recipient (or every member of the team) has been told, then collected, returned or refused. received and notified are both waiting. The recipient's collection code is never in an event or the API.
delivery.received
A parcel is logged for a person, a team or the front desk: by reception in the Console or Agoo Workspace, a guard at the gate, a courier at a kiosk or from a drop-off poster (source), or your system through the API. A parcel logged offline arrives when the device reconnects, with its original received_at.
data.object: Delivery · Full schema: delivery.received
{
"id": "evt_01m4x0bj50ftysd56z6pbzq7jp",
"type": "delivery.received",
"created_at": "2026-10-14T10:47:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "delivery_01m4x0bj50e43vcf8bz0ar7qhp",
"status": "received",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"recipient_type": "person",
"recipient_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"team_id": null,
"for": "Kwame Mensah, Treasury",
"kind": "envelope",
"location": "Shelf B",
"sender": "Coastline Consult",
"package_count": 1,
"source": "console",
"received_at": "2026-10-14T10:47:00Z"
}
}
}delivery.collected
A parcel is handed over: with the recipient's code, their signature, a member of staff's confirmation with a reason, or through the API (collection_method: code, signature, staff or api).
data.object: Delivery · Full schema: delivery.collected
{
"id": "evt_01m4x5dkpgfx8srrg2sjq59adq",
"type": "delivery.collected",
"created_at": "2026-10-14T12:15:30Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "delivery_01m4x0bj50e43vcf8bz0ar7qhp",
"status": "collected",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"recipient_type": "person",
"recipient_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"received_at": "2026-10-14T10:47:00Z",
"collected_at": "2026-10-14T12:15:30Z",
"collection_method": "code",
"collected_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"collected_by_name": null,
"handed_over_by": "person_01kjsevr70faybwar4k1wjj6x2"
}
}
}delivery.updated
Reception corrects a waiting parcel: who it's for, where it's kept or what it is. A new recipient starts again at received with a new collection code, and is told unless reception chose not to.
data.object: Delivery · Full schema: delivery.updated
{
"id": "evt_01m4x2c8m0fz7nq3d6w5bt8r1k",
"type": "delivery.updated",
"created_at": "2026-10-14T11:02:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "delivery_01m4x0bj50e43vcf8bz0ar7qhp",
"status": "received",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"recipient_type": "team",
"recipient_id": null,
"team_id": "team_01kjsf0b2gf3mq8w4vz7xnsk1e",
"for": "Finance",
"location": "Shelf B",
"updated_at": "2026-10-14T11:02:00Z"
}
}
}delivery.returned
A waiting parcel is sent back to the sender or the courier (status returned), or turned away at the door (refused), with the return_reason if one was given.
data.object: Delivery · Full schema: delivery.returned
{
"id": "evt_01m4z6q2r0e8t3k9b5x1vh7nda",
"type": "delivery.returned",
"created_at": "2026-10-17T09:02:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "delivery_01m4x0bj50e43vcf8bz0ar7qhp",
"status": "returned",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"recipient_type": "person",
"recipient_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"reminders_sent": 3,
"returned_at": "2026-10-17T09:02:00Z",
"return_reason": "Not collected after three reminders"
}
}
}Bookings
booking.created
Someone books a slot, on a booking page, in an embed or through the API. It fires for every booking. status is pending when the booking type requires the host's confirmation, and confirmed otherwise. Confirmed in-person bookings also fire visit.created for the linked visit; for a pending booking, that happens when the host confirms it.
data.object: Booking · Full schema: booking.created
{
"id": "evt_01m4dtqd00f2nsmgf8nvxm1h2r",
"type": "booking.created",
"created_at": "2026-10-08T13:20:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4dtqd00eq4rtc1jdsr1gr6z",
"status": "confirmed",
"booking_type_id": "btype_01kn44arm0eya81713cpbnfbpq",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"start_at": "2026-10-16T10:00:00Z",
"end_at": "2026-10-16T10:30:00Z",
"meeting_kind": "in_person",
"attendee": {
"name": "Ama Owusu",
"phone": "+233241234567",
"email": "ama@coastline.example",
"company": "Coastline Consult"
},
"visit_id": "visit_01m4dtqdz8f59rarmdksqjprz9"
}
}
}booking.confirmed
The host confirms a pending booking, in the console, in the Workspace app, from the link in their confirmation request or through the API. Bookings that don't need confirmation fire only booking.created, with status: "confirmed". In-person bookings also fire visit.created now, for the visit created with the confirmation.
data.object: Booking · Full schema: booking.confirmed
{
"id": "evt_01m4th3ht0f7x8n6y4aca84p3h",
"type": "booking.confirmed",
"created_at": "2026-10-13T11:42:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4t4p6z0f58tbhgwq3sywrf8",
"status": "confirmed",
"booking_type_id": "btype_01kqs3efm0emma1643wwgmw0jc",
"start_at": "2026-10-20T10:00:00Z",
"end_at": "2026-10-20T10:45:00Z",
"visit_id": "visit_01m4th3ht0ej1vwm19q7zs06rr",
"confirm_by": "2026-10-14T08:05:00Z",
"confirmed_at": "2026-10-13T11:42:00Z"
}
}
}booking.declined
The host declines a pending booking. The slot is free again, and the attendee is told their request wasn't accepted. decline_reason is for your organisation: the attendee sees it only if the host chose to share it.
data.object: Booking · Full schema: booking.declined
{
"id": "evt_01m4tjzzd0fj3b2yraeyxpgrex",
"type": "booking.declined",
"created_at": "2026-10-13T12:15:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4t8d7j0f47vwvc95qzqkdf6",
"status": "declined",
"booking_type_id": "btype_01kqs3efm0emma1643wwgmw0jc",
"start_at": "2026-10-21T14:00:00Z",
"visit_id": null,
"declined_at": "2026-10-13T12:15:00Z",
"decline_reason": "Kwame is at the Kumasi Office that week."
}
}
}booking.expired
A pending booking wasn't confirmed in time: the booking type's confirmation window passed, or the meeting's start time came first. confirm_by shows when. The slot is free again, and the attendee is told their request couldn't be confirmed.
data.object: Booking · Full schema: booking.expired
{
"id": "evt_01m4xkzkt0fwxrg79zbpy7ewsb",
"type": "booking.expired",
"created_at": "2026-10-14T16:30:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4v1jwt0emvtprkfgc8pzb68",
"status": "expired",
"booking_type_id": "btype_01kqs3efm0emma1643wwgmw0jc",
"start_at": "2026-10-22T09:00:00Z",
"visit_id": null,
"confirm_by": "2026-10-14T16:30:00Z"
}
}
}booking.rescheduled
A confirmed booking moves to a new time, by the attendee's reschedule link, the host or the API.
data.object: Booking · Full schema: booking.rescheduled
{
"id": "evt_01m4fx58b0e2hbybxcz34ze8bn",
"type": "booking.rescheduled",
"created_at": "2026-10-09T08:41:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4dtqd00eq4rtc1jdsr1gr6z",
"status": "confirmed",
"booking_type_id": "btype_01kn44arm0eya81713cpbnfbpq",
"host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"start_at": "2026-10-16T14:30:00Z",
"end_at": "2026-10-16T15:00:00Z",
"visit_id": "visit_01m4dtqdz8f59rarmdksqjprz9"
}
}
}booking.cancelled
A booking is cancelled: a confirmed booking by the attendee's cancel link, the host or the API, or a pending request by the attendee's cancel link. In-person bookings that had a visit also fire visit.cancelled.
data.object: Booking · Full schema: booking.cancelled
{
"id": "evt_01m4rh0rt0enb847ysybbtyj4f",
"type": "booking.cancelled",
"created_at": "2026-10-12T17:02:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "booking_01m4dtqd00eq4rtc1jdsr1gr6z",
"status": "cancelled",
"start_at": "2026-10-16T14:30:00Z",
"visit_id": "visit_01m4dtqdz8f59rarmdksqjprz9",
"cancelled_at": "2026-10-12T17:02:00Z",
"cancellation_reason": "Kwame is travelling that day."
}
}
}Attendance
attendance.clocked_in
An employee clocks in, by any method. Punches made offline fire when the device syncs, with their original occurred_at. A clock-in a manager added in the Console, for one the person missed or to correct a time, has the method manual and added: who added it (by), their reason, and in a correction the punch it replaces (which fires attendance.voided). The object carries evidence only when the punch has it: selfie_file_id, location, wifi, and face_check with review (see evidence and notices). Face punches carry face_check: "matched"; Agoo never sends face images or templates.
data.object: Attendance event · Full schema: attendance.clocked_in
{
"id": "evt_01m4wq6yw0ekmbj8816hhhdf3w",
"type": "attendance.clocked_in",
"created_at": "2026-10-14T08:07:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4wq6yw0ez5srdzr46tftpah",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "clock_in",
"method": "face",
"occurred_at": "2026-10-14T08:07:12Z",
"shift_id": "shift_01kn6pqfm0fmasbh61jnr3syze",
"late_minutes": 7,
"offline": false,
"face_check": "matched"
}
}
}attendance.clocked_out
An employee clocks out, by any method. A clock-out a manager added in the Console has the method manual and added, as for attendance.clocked_in.
data.object: Attendance event · Full schema: attendance.clocked_out
{
"id": "evt_01m4xnzdsrfhw9jgnmd6xjx1xw",
"type": "attendance.clocked_out",
"created_at": "2026-10-14T17:04:51Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4xnzdsreh7a3gbm0xtrx108",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "clock_out",
"method": "qr",
"occurred_at": "2026-10-14T17:04:51Z",
"shift_id": "shift_01kn6pqfm0fmasbh61jnr3syze",
"late_minutes": null,
"offline": false
}
}
}attendance.break_started
Someone starts a break, where your organisation has break punches on: at a kiosk, in the Agoo Workspace app, from your terminal, or a manager adds a Start break they missed (method manual). kind is break_start, and break_type is rest (a break) or nursing (the paid nursing hour). They're still clocked in and on site.
data.object: Attendance event · Full schema: attendance.break_started
{
"id": "evt_01m4xa2kr8f0jw3m6zq9k2n1vd",
"type": "attendance.break_started",
"created_at": "2026-10-14T12:31:06Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4xa2kr8ezq7rm2ycx5c0tsw",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "break_start",
"break_type": "rest",
"method": "qr",
"occurred_at": "2026-10-14T12:31:06Z",
"shift_id": null,
"late_minutes": null,
"offline": false
}
}
}attendance.break_ended
Someone ends a break. break_type is the kind of break ended. How long it was, and what came off worked time, is in the day's figures in your timesheet export, not in the event. A break not ended before the clock-out sends no event: the clock-out closes it, and it's flagged for a manager.
data.object: Attendance event · Full schema: attendance.break_ended
{
"id": "evt_01m4xcfvm0f9h3wq7x3k5r8bge",
"type": "attendance.break_ended",
"created_at": "2026-10-14T13:13:40Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4xcfvm0efr2a8n6hvmd5j3k",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "break_end",
"break_type": "rest",
"method": "qr",
"occurred_at": "2026-10-14T13:13:40Z",
"shift_id": null,
"late_minutes": null,
"offline": false
}
}
}attendance.late
A clock-in is later than the person's shift start plus its grace period. It fires after attendance.clocked_in for the same clock-in; late_minutes says how late.
data.object: Attendance event · Full schema: attendance.late
{
"id": "evt_01m4wq6zv8fsa9yhnee5bck2jt",
"type": "attendance.late",
"created_at": "2026-10-14T08:07:13Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4wq6yw0ez5srdzr46tftpah",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "clock_in",
"method": "face",
"occurred_at": "2026-10-14T08:07:12Z",
"shift_id": "shift_01kn6pqfm0fmasbh61jnr3syze",
"late_minutes": 7,
"offline": false,
"face_check": "matched"
}
}
}attendance.voided
A punch stops counting: a manager voided it in the Console, rejected it after a failed face match, or corrected its time. Drop it from your records; listing attendance events no longer returns it. voided says when, who and why. A correction is two events: this one for the wrong punch, with voided.replaced_by, and attendance.clocked_in or attendance.clocked_out for the punch at the right time (method manual, with added.replaces naming this one).
data.object: Attendance event · Full schema: attendance.voided
{
"id": "evt_01m50c1vy8f2x0z1n4pqfbq6tw",
"type": "attendance.voided",
"created_at": "2026-10-15T09:12:40Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "clock_01m4xnzdsreh7a3gbm0xtrx108",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "clock_out",
"method": "qr",
"occurred_at": "2026-10-14T17:04:51Z",
"shift_id": "shift_01kn6pqfm0fmasbh61jnr3syze",
"late_minutes": null,
"offline": false,
"voided": {
"at": "2026-10-15T09:12:40Z",
"by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"reason": "Scanned out at 17:04 but stayed until 18:30 for the audit.",
"replaced_by": "clock_01m50c1vy8f2x0z1n4pqfbq6tv"
}
}
}
}attendance.absent
Someone's rostered shift has ended and they didn't clock in, weren't on approved leave, and it isn't a public holiday your organisation observes. Agoo never calls anyone absent before their shift ends: until then they're "not in yet", which the attendance summary counts as not_in_yet. It fires once per person and day, within a few minutes of the shift's end. If a manager later adds the clock-in they missed, the day becomes a worked one and attendance.clocked_in fires; this event isn't withdrawn.
data.object: Attendance absence · Full schema: attendance.absent
{
"id": "evt_01m4yb6n00e7zt3hgk2r6wfq4x",
"type": "attendance.absent",
"created_at": "2026-10-14T17:03:10Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"date": "2026-10-14",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"shift_id": "shift_01kn6pqfm0fmasbh61jnr3syze",
"shift_starts_at": "2026-10-14T08:00:00Z",
"shift_ends_at": "2026-10-14T17:00:00Z"
}
}
}Leave
leave.requested
Someone asks for leave that needs approval, in the Agoo Workspace app. It fires once per request, when it's sent; approval says which step it's on and how many there are. Leave that needs no approval (a type your organisation records rather than approves, or no approval step) fires leave.approved instead.
data.object: Leave request
{
"id": "evt_01m4yd2m00e1x7ah3v9kq4tz6c",
"type": "leave.requested",
"created_at": "2026-10-14T09:12:04Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "leave_01m4yd2k80f3s9tq7wbe5x6hzn",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"type_id": "ltype_01kjpt9c40e8ra0j6s3m1v7wqd",
"type_name": "Annual leave",
"kind": "annual",
"private": false,
"status": "pending",
"starts_on": "2026-11-02",
"ends_on": "2026-11-06",
"days": 5,
"approval": { "status": "pending", "step": 1, "of": 2 }
}
}
}leave.approved
Leave is approved: the request's last approval step is decided, HR records leave in the Console, your system records it with POST /leave-requests, or it needed no approval. An approval part-way through the steps (the line manager before HR, say) doesn't fire it.
data.object: Leave request
{
"id": "evt_01m4ygq7c0fb2tk8e3wzs5n1rd",
"type": "leave.approved",
"created_at": "2026-10-14T15:40:31Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "leave_01m4yd2k80f3s9tq7wbe5x6hzn",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"type_id": "ltype_01kjpt9c40e8ra0j6s3m1v7wqd",
"type_name": "Annual leave",
"kind": "annual",
"private": false,
"status": "approved",
"starts_on": "2026-11-02",
"ends_on": "2026-11-06",
"days": 5,
"decided_at": "2026-10-14T15:40:31Z"
}
}
}leave.declined
An approver declines a leave request. decision_note is the reason, which the person sees too.
data.object: Leave request
{
"id": "evt_01m4ygw3m0e6q9rb2xkt7vh5fs",
"type": "leave.declined",
"created_at": "2026-10-14T15:44:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "leave_01m4yd2k80f3s9tq7wbe5x6hzn",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"type_id": "ltype_01kjpt9c40e8ra0j6s3m1v7wqd",
"type_name": "Annual leave",
"kind": "annual",
"private": false,
"status": "declined",
"starts_on": "2026-11-02",
"ends_on": "2026-11-06",
"days": 5,
"decision_note": "We're short-staffed that week. Could you take the week after?",
"decided_at": "2026-10-14T15:44:12Z"
}
}
}leave.cancelled
A waiting request, or approved leave that hasn't started, is cancelled: by the person in the Workspace app, by HR in the Console, or by your system. Its days go back to the balance.
data.object: Leave request
{
"id": "evt_01m4yh5q80fz3c6pd1ra8mwk2e",
"type": "leave.cancelled",
"created_at": "2026-10-15T08:03:55Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "leave_01m4yd2k80f3s9tq7wbe5x6hzn",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"type_id": "ltype_01kjpt9c40e8ra0j6s3m1v7wqd",
"type_name": "Annual leave",
"kind": "annual",
"private": false,
"status": "cancelled",
"starts_on": "2026-11-02",
"ends_on": "2026-11-06",
"days": 5
}
}
}Private kinds of leave
For a type your organisation marks private (sick and maternity leave by default), private is true, kind is away, type_name is
"Away" and type_id is null, with no reason or decision_note, so your integration never learns what the leave is. Certificates are
never sent.
Timesheets
A timesheet is one person's hours for a period (a week, a fortnight or a month, as the organisation chooses), approved step by step and then locked. totals holds the period's numbers in seconds, minutes and days; private kinds of leave appear in totals.leave as away. Read a timesheet's days with GET /timesheets/{timesheet_id}.
timesheet.submitted
A timesheet goes for approval: when the person confirms it, when it's sent without their confirmation (their time to confirm ran out, or an approver sent it), or when it starts a new round after being sent back to the first step or reopened. approval says which step it's on and how many there are; round counts from 1. With no approval steps, the timesheet waits to lock itself and this event doesn't fire.
data.object: Timesheet
{
"id": "evt_01m52a9p40e3s7kq2xbw6t5rvd",
"type": "timesheet.submitted",
"created_at": "2026-10-02T09:00:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "ts_01m52a7k40e9r3tq6wbx5c8hzp",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"period": { "id": "tsp_01m3x9c2h0f4n7ds8kq2v5rt1a", "starts_on": "2026-09-01", "ends_on": "2026-09-30", "payroll_on": "2026-10-05" },
"status": "to_approve",
"round": 1,
"confirmed_at": "2026-10-02T09:00:12Z",
"submitted_at": "2026-10-02T09:00:12Z",
"locked": false,
"approval": { "status": "pending", "step": 1, "of": 2 }
}
}
}timesheet.approved
A timesheet is approved and locked: its last approval step is decided, your system approves it with POST /timesheets/{timesheet_id}/approve, or, with no approval steps, it locks itself a few days after its period. An approval part-way through the steps (the line manager before HR, say) doesn't fire it. approved_via is person, api or auto, and totals are frozen from now on.
data.object: Timesheet
{
"id": "evt_01m52hq6c0f8t2kr5xwz3n9bde",
"type": "timesheet.approved",
"created_at": "2026-10-03T14:21:40Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "ts_01m52a7k40e9r3tq6wbx5c8hzp",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"period": { "id": "tsp_01m3x9c2h0f4n7ds8kq2v5rt1a", "starts_on": "2026-09-01", "ends_on": "2026-09-30", "payroll_on": "2026-10-05" },
"status": "approved",
"round": 1,
"approved_at": "2026-10-03T14:21:40Z",
"approved_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"approved_via": "person",
"locked": true,
"changed_after_lock": false,
"totals": { "worked_seconds": 633600, "regular_seconds": 576000, "overtime_daily_seconds": 9000, "overtime_weekly_seconds": 0, "absences": 0 }
}
}
}timesheet.reopened
An approved timesheet is reopened, in the Console or with POST /timesheets/{timesheet_id}/reopen. reopen_reason is the reason, which the person sees too. It goes back to its first approval step as a new round, so a timesheet.submitted follows, then a new timesheet.approved when it's approved again. If you had already sent this period to payroll, treat the next timesheet.approved as a correction.
data.object: Timesheet
{
"id": "evt_01m54r2d80e6k9tq3xbv7s1wmc",
"type": "timesheet.reopened",
"created_at": "2026-10-06T10:02:18Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "ts_01m52a7k40e9r3tq6wbx5c8hzp",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"period": { "id": "tsp_01m3x9c2h0f4n7ds8kq2v5rt1a", "starts_on": "2026-09-01", "ends_on": "2026-09-30", "payroll_on": "2026-10-05" },
"status": "to_approve",
"round": 2,
"locked": false,
"reopened_at": "2026-10-06T10:02:18Z",
"reopen_reason": "Missed clock-out on 29 Sep added after payroll",
"approval": { "status": "pending", "step": 1, "of": 2 }
}
}
}Locked timesheets and late punches
A punch made offline before a timesheet was locked can sync after it. Agoo keeps it but doesn't count it until the timesheet is reopened, and
changed_after_lock becomes true; attendance.clocked_in and attendance.clocked_out still fire for it. Changes your system tries to make
on a locked day, such as recording leave, return invalid_state.
Payroll exports
A payroll export is one file of a period's approved, locked timesheets by your organisation's pay codes, made by HR, an Owner or an Admin in the Console, or by your system with POST /payroll-exports. Payroll exports come with the payroll export, on Pro and Enterprise. The event carries the export's details, never its lines: download the file with GET /payroll-exports/{payroll_export_id}/file, which needs attendance:payroll, because payroll files name every kind of leave.
payroll_export.created
A payroll file is made. number counts your organisation's payroll exports from 1. site_ids is null for the whole organisation. layout is long, wide or custom (one of your own layouts, set in the Console). people counts the people in the file and lines its lines; corrections counts the people whose lines correct an earlier file for the same period, and superseded the people in this file whose lines a later file has corrected (0 when it's made). file.sha256 is the checksum of the kept file: downloading it again gives the same bytes. created_via is person or api, and created_by is null when your system made it.
data.object: Payroll export
{
"id": "evt_01m5c8r3a0f2n6tq4xbz7k9hdw",
"type": "payroll_export.created",
"created_at": "2026-10-03T15:02:11Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "payroll_export_01m5c8r2t40e7k3q9xbv6s1wnd",
"number": 3,
"period": { "id": "tsp_01m3x9c2h0f4n7ds8kq2v5rt1a", "starts_on": "2026-09-01", "ends_on": "2026-09-30", "payroll_on": "2026-10-05" },
"site_ids": null,
"format": "xlsx",
"layout": "long",
"people": 42,
"corrections": 0,
"lines": 168,
"superseded": 0,
"file": {
"name": "volta-bank-payroll-september-2026-3.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"byte_size": 48213,
"sha256": "9f2b6c41d0e8a7b35c9e1f04a6d2b8c7e3f5a1d9b0c4e6f8a2d7b1c3e5f9a0b4"
},
"created_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"created_via": "person",
"created_at": "2026-10-03T15:02:10Z"
}
}
}Corrections, not changed files
A payroll file never changes once made. If a timesheet in it is reopened and approved again, the next file for that period carries the difference
as correction lines, and the earlier export's superseded counts the people corrected. Watch timesheet.reopened and timesheet.approved to know when to make
another.
Reviews
Events about your organisation's page on Agoo Reviews, with the review as data.object, as GET /reviews/{review_id} returns it. They come with Reviews Business (Growth, Pro and Enterprise) and read with reviews:read. label says what Agoo checked about the person behind a review (verified_visit, invited, phone_confirmed or email_confirmed), never that it is true, and labels never change your score. A review never carries the reviewer's contact details, and nothing tells you which invitation led to which review.
review.published
A review appears on your page, after Agoo's checks. Also fires when a review that was held or under dispute comes back.
data.object: Review
{
"id": "evt_01m6b3c5a0f2n6tq4xbz7k9hdw",
"type": "review.published",
"created_at": "2026-10-14T10:02:01Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "review_01m6b3c4d5e6f7g8h9jkmnpqrs",
"object": "review",
"business_id": "rbiz_01m6a1b2c3d4e5f6g7h8jkmnpq",
"place": { "id": "rplace_01m6a2b3c4d5e6f7g8h9jkmnpq", "name": "Osu", "town": "Accra", "site_id": "site_01kjq3m7v0f5s9q2y8b4n6c1d3" },
"status": "published",
"stars": 4,
"title": "Quick and friendly",
"body": "I was seen within ten minutes and the receptionist explained everything clearly.",
"visit_month": "2026-10",
"language": "en",
"label": "verified_visit",
"proof_seen": false,
"reviewer_name": "Esi O.",
"reply": null,
"url": "https://agoo.reviews/coastline-consult/osu",
"published_at": "2026-10-14T10:02:00Z",
"edited_at": null
}
}
}review.updated
The reviewer changes a published review: its stars, title or words. edited_at says when. Edits are checked again before they show.
data.object: Review, as in review.published.
review.removed
A published review comes down. status is removed (a moderator removed it, with the published reason in removal_reason) or withdrawn (its author took it down or deleted their account); a review held for a check also fires this, and review.published when it returns. The words are left out.
data.object: Review
{
"id": "evt_01m6b3d8a0f2n6tq4xbz7k9hdw",
"type": "review.removed",
"created_at": "2026-10-16T08:40:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "review_01m6b3c4d5e6f7g8h9jkmnpqrs",
"object": "review",
"business_id": "rbiz_01m6a1b2c3d4e5f6g7h8jkmnpq",
"place": { "id": "rplace_01m6a2b3c4d5e6f7g8h9jkmnpq", "name": "Osu", "town": "Accra", "site_id": "site_01kjq3m7v0f5s9q2y8b4n6c1d3" },
"status": "removed",
"removal_reason": "personal_data",
"published_at": "2026-10-14T10:02:00Z"
}
}
}review.replied
Your reply is published (after the same checks as reviews), from the Console or with POST /reviews/{review_id}/reply, or a published reply is changed. reply carries it.
data.object: Review, with reply set: { "body": "Thank you, Esi. We have added a second teller at lunchtime.", "status": "published", "published_at": "2026-10-14T12:00:00Z", "updated_at": "2026-10-14T12:00:00Z" }.
Rosters
Roster events need rosters, on Growth and above. Offers, answers, withdrawals and swaps or claims that are called off don't fire events: read them with GET /shift-swaps and GET /shift-claims (Pro and above).
roster.published
A manager publishes a site's roster in the Console, or your system publishes it with POST /rosters/publish. It fires once per publish, with how many shifts were added, changed and removed and how many people were told. Read the shifts themselves with GET /roster-shifts. New days that a published pattern adds as time passes are published without a publish of their own, and don't fire it.
data.object: Roster publication
{
"id": "evt_01m5cmm8yjekp0am4ge8pq1d8j",
"type": "roster.published",
"created_at": "2026-10-16T16:05:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "roster_pub_01m5k4qexa6kwfsr9nhy4r7w5z",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"published_at": "2026-10-16T16:05:12Z",
"published_by": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"added": 14,
"changed": 3,
"removed": 1,
"open": 0,
"people": 9,
"note": "Covering the Main gate while Kofi is on a course"
}
}
}shift_swap.requested
A swap needs approval: a colleague has accepted a shift someone offered, and it now waits for whoever approves swaps. approval says which step it's on and how many there are. A swap that needs no approval (your organisation chose No approval, or lets swaps between people on the same shift template stand) fires shift_swap.approved instead.
data.object: Shift swap
{
"id": "evt_01m5pwf66fs307rvbvh0fsfjj4",
"type": "shift_swap.requested",
"created_at": "2026-10-19T10:42:08Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_swap_01m5bsm3s6ta3zbtb975p6a2ws",
"status": "accepted",
"shift": {
"id": "roster_shift_01m5q2msv9qr4x510cytkqzd0e",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"person_id": "person_01m5y2d63x571ncmshhj0j6prq",
"on_date": "2026-10-23",
"starts_at": "2026-10-23T20:00:00Z",
"ends_at": "2026-10-24T08:00:00Z",
"start_time": "20:00",
"end_time": "08:00",
"name": "Night gate",
"label": "Main gate"
},
"return_shift": null,
"from_person_id": "person_01m5y2d63x571ncmshhj0j6prq",
"to_person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"taker_person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"note": "Family wedding in Kumasi that weekend",
"approval": { "status": "pending", "step": 1, "of": 1 }
}
}
}shift_swap.approved
A swap is approved: its last approval step is decided, your system decides it with POST /shift-swaps/{shift_swap_id}/decide, or it needed no approval. The roster has already changed: shift (and return_shift, in an exchange) now belongs to the other person.
data.object: Shift swap
{
"id": "evt_01m5ax85mkb6z3djryb521ka2k",
"type": "shift_swap.approved",
"created_at": "2026-10-19T14:03:51Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_swap_01m5bsm3s6ta3zbtb975p6a2ws",
"status": "approved",
"shift": {
"id": "roster_shift_01m5q2msv9qr4x510cytkqzd0e",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"on_date": "2026-10-23",
"start_time": "20:00",
"end_time": "08:00",
"name": "Night gate"
},
"return_shift": null,
"from_person_id": "person_01m5y2d63x571ncmshhj0j6prq",
"taker_person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"decided_at": "2026-10-19T14:03:51Z"
}
}
}shift_swap.declined
An approver declines a swap. decision_note is the reason, which both people see too. The shift stays with the person who offered it.
data.object: Shift swap
{
"id": "evt_01m5zemmvkwz34y7aw6t5rbpk0",
"type": "shift_swap.declined",
"created_at": "2026-10-19T14:10:27Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_swap_01m5bsm3s6ta3zbtb975p6a2ws",
"status": "declined",
"from_person_id": "person_01m5y2d63x571ncmshhj0j6prq",
"taker_person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"decision_note": "Nii is already on the day shift before it. Could someone else take it?",
"decided_at": "2026-10-19T14:10:27Z"
}
}
}shift_claim.requested
Someone claims an open shift and the claim needs approval. Where your organisation gives an open shift to the first person to claim it, the claim fires shift_claim.approved at once instead.
data.object: Shift claim
{
"id": "evt_01m5rkpvjvw1a7hhggxgdhvdf8",
"type": "shift_claim.requested",
"created_at": "2026-10-17T07:21:40Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_claim_01m54k55wjfe13p30bjq7zwj3n",
"status": "pending",
"shift": {
"id": "roster_shift_01m5x6yn6f3w6q5jgfsfdabrqn",
"site_id": "site_01knvdere0f738bjjhnyhtyv2z",
"person_id": null,
"open": true,
"on_date": "2026-10-24",
"start_time": "08:00",
"end_time": "20:00",
"name": "Day gate",
"label": "Main gate"
},
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"note": "Happy to cover it",
"approval": { "status": "pending", "step": 1, "of": 1 }
}
}
}shift_claim.approved
A claim is approved: its last approval step is decided, your system decides it with POST /shift-claims/{shift_claim_id}/decide, or it was the first claim where the first claim takes the shift. The open shift is now the person's. Any other claims on it are closed as taken, without an event of their own.
data.object: Shift claim
{
"id": "evt_01m5dqtjk9j7r6384nf539h74y",
"type": "shift_claim.approved",
"created_at": "2026-10-17T09:02:13Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_claim_01m54k55wjfe13p30bjq7zwj3n",
"status": "approved",
"shift": {
"id": "roster_shift_01m5x6yn6f3w6q5jgfsfdabrqn",
"site_id": "site_01knvdere0f738bjjhnyhtyv2z",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"open": false,
"on_date": "2026-10-24",
"start_time": "08:00",
"end_time": "20:00",
"name": "Day gate"
},
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"decided_at": "2026-10-17T09:02:13Z"
}
}
}shift_claim.declined
An approver declines a claim. decision_note is the reason, which the person sees too. The shift stays open for others to claim.
data.object: Shift claim
{
"id": "evt_01m54k9kek1yw630yk2m3htqz6",
"type": "shift_claim.declined",
"created_at": "2026-10-17T09:05:44Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "shift_claim_01m54k55wjfe13p30bjq7zwj3n",
"status": "declined",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"decision_note": "You'd have under 12 hours' rest after your night shift.",
"decided_at": "2026-10-17T09:05:44Z"
}
}
}Consent stays private
The Labour Act's consent warnings for night work and overtime come from maternity leave, which can be private, so they're never sent to your systems, and nor are the records of consent HR keeps.
Watchlist
watchlist.matched
A visit created for, or someone checking in as, a person on your watchlist matches an entry. Agoo holds the visit and alerts your security team silently at the same time; the visitor isn't told. Only endpoints that list watchlist.matched receive it: * doesn't cover it.
data.object: Watchlist match · Full schema: watchlist.matched
{
"id": "evt_01m4x88vm0f8cb5j9w0eqpn59n",
"type": "watchlist.matched",
"created_at": "2026-10-14T13:05:20Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"watchlist_entry_id": "watch_01m0fqhdr0e31ray1qxa2x5vtk",
"visit_id": "visit_01m4x88hvgf28a4tk6b4egxkvz",
"visitor_id": "visitor_01m4x88830e36bdvkkxs8gvd0b",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"matched_on": ["name"],
"score": 0.92,
"matched_at": "2026-10-14T13:05:20Z"
}
}
}Roll calls
roll_call.started
A roll call starts at a site, from the Workspace app, the console or the API.
data.object: Roll call · Full schema: roll_call.started
{
"id": "evt_01m4x13bw0fgj8cqb0dth1bdmn",
"type": "roll_call.started",
"created_at": "2026-10-14T11:00:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "rollcall_01m4x13bw0fjmr5ekjkshzkke6",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "drill",
"status": "active",
"counts": {
"total": 46,
"accounted_for": 0,
"missing": 46
},
"started_at": "2026-10-14T11:00:00Z",
"started_by": "person_01kjsexjt0e9rt8r69a60jzx03"
}
}
}roll_call.updated
Someone is accounted for (by their "I'm safe" link or by a marshal), so the counts change.
data.object: Roll call · Full schema: roll_call.updated
{
"id": "evt_01m4x197c0fm3s8zmebv9gr14v",
"type": "roll_call.updated",
"created_at": "2026-10-14T11:03:12Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "rollcall_01m4x13bw0fjmr5ekjkshzkke6",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "drill",
"status": "active",
"counts": {
"total": 46,
"accounted_for": 12,
"missing": 34
}
}
}
}roll_call.closed
A roll call is closed. counts are final.
data.object: Roll call · Full schema: roll_call.closed
{
"id": "evt_01m4x297s8e2yaadpyy9v88bny",
"type": "roll_call.closed",
"created_at": "2026-10-14T11:20:41Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "rollcall_01m4x13bw0fjmr5ekjkshzkke6",
"site_id": "site_01kjpt3yw0fz0v414608h9x65s",
"kind": "drill",
"status": "closed",
"counts": {
"total": 46,
"accounted_for": 46,
"missing": 0
},
"closed_at": "2026-10-14T11:20:41Z",
"closed_by": "person_01kjsexjt0e9rt8r69a60jzx03"
}
}
}Departments and positions
Departments and positions are kept by HR, an Owner or an Admin in the Console, or by your system with the People and HR endpoints. Positions are part of Growth, Pro and Enterprise.
department.created
A department is made, in the Console or through the API.
data.object: Department · Full schema: department.created
{
"id": "evt_01m6g8a2c4e6g8j0k2m4n6p8qr",
"type": "department.created",
"created_at": "2026-10-19T09:00:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "dept_01kjsd2v80e5q7m3c9x4ht6wfa",
"object": "department",
"name": "Treasury",
"code": "TRY",
"parent_id": "dept_01kjsd1qg0f2n8r5bz7ke3yc4d",
"head_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"archived": false,
"people": 0
}
}
}department.updated
A department changes: it's renamed (everyone in it gets the new name as their department), moved under another department, given a new head, code or cost centre, archived or brought back. data.previous_attributes.changed lists what changed (name, code, parent, head, cost_centre); it's left out when the department is archived or brought back, so read archived.
data.object: Department · Full schema: department.updated
{
"id": "evt_01m6g9b3d5f7h9k1m3n5p7r9st",
"type": "department.updated",
"created_at": "2026-10-19T10:15:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "dept_01kjsd2v80e5q7m3c9x4ht6wfa",
"object": "department",
"name": "Treasury",
"cost_centre": "CC-215",
"archived": false,
"people": 14,
"updated_at": "2026-10-19T10:15:00Z"
},
"previous_attributes": { "changed": ["cost_centre"] }
}
}position.created
A position is made, in the Console or through the API. It starts vacant unless it was made frozen or closed.
data.object: Position · Full schema: position.created
{
"id": "evt_01m6ga4c6e8g0j2k4m6n8q0r2t",
"type": "position.created",
"created_at": "2026-10-12T08:40:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "pos_01m6h2k4q8e7r9t1v3w5x7y9za",
"object": "position",
"title": "Senior operations officer",
"department_id": "dept_01kjsd2v80e5q7m3c9x4ht6wfb",
"reports_to_id": "pos_01m6h1a3c5e7g9j1k3m5n7p9qr",
"grade": "G6",
"status": "vacant",
"holder_id": null
}
}
}position.updated
A position changes: its title, department, site, grade, the position it reports to, its full-time equivalent or whether it's budgeted, or it's frozen, closed or reopened. data.previous_attributes.changed lists what changed (title, department, site, reports_to, grade, fte, budgeted, status). Someone starting or leaving the position doesn't fire it: that's person.job_changed.
data.object: Position · Full schema: position.updated
{
"id": "evt_01m6gb5d7f9h1k3m5n7p9r1s3v",
"type": "position.updated",
"created_at": "2026-10-19T10:20:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "pos_01m6h2k4q8e7r9t1v3w5x7y9za",
"object": "position",
"title": "Senior operations officer",
"status": "frozen",
"holder_id": null,
"updated_at": "2026-10-19T10:20:00Z"
},
"previous_attributes": { "changed": ["status"] }
}
}People
person.created
A host or employee is added: in the console, by CSV import, by directory sync or through the API.
data.object: Person · Full schema: person.created
{
"id": "evt_01kjw4mh78f7vtyd390ky5chrw",
"type": "person.created",
"created_at": "2026-03-04T10:00:01Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"name": "Nii Armah",
"email": "nii.armah@voltabank.example",
"department": "Operations",
"can_host": true,
"tracks_attendance": true,
"works_gate": false,
"status": "active"
}
}
}person.updated
A person's details change.
data.object: Person · Full schema: person.updated
{
"id": "evt_01m48sy4j0fhsa3qasnxrpxgef",
"type": "person.updated",
"created_at": "2026-10-06T14:30:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "person_01kjsesxm0e4zbyvkr7bcfxbfg",
"name": "Kwame Mensah",
"job_title": "Director, Treasury",
"department": "Treasury",
"status": "active",
"updated_at": "2026-10-06T14:30:00Z"
}
}
}person.deactivated
A person is deactivated. Their history stays; they can no longer host, clock in or sign in.
data.object: Person · Full schema: person.deactivated
{
"id": "evt_01m0fq88s0fyj8r6ee2nc21qa3",
"type": "person.deactivated",
"created_at": "2026-08-20T13:55:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "person_01kjsf1800f38sgy433fabefh9",
"name": "Kojo Badu",
"status": "deactivated",
"deactivated_at": "2026-08-20T13:55:00Z"
}
}
}person.reactivated
A deactivated person is reactivated, in the console. They're back in the directory and can host and clock in again; if deactivating them removed their sign-in, it's back too.
data.object: Person · Full schema: person.reactivated
{
"id": "evt_01m2hb3qp0fs7r1ax0n6d9kt4e",
"type": "person.reactivated",
"created_at": "2026-09-14T08:12:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "person_01kjsf1800f38sgy433fabefh9",
"name": "Kojo Badu",
"status": "active",
"deactivated_at": null
}
}
}person.job_changed
A change to someone's job takes effect: their job title, grade, department, position, line manager, sites, employment type, contract or contracted hours. It fires when the change is applied: at once when it's for today or earlier, otherwise on its date, whether HR or your system made it or an approver approved a line manager's proposal. fields lists what changed; the rest is their job as it now stands. The change itself, with each field's before and after, is at GET/people/{person_id}/job-changes.
data.object: Job change · Full schema: person.job_changed
{
"id": "evt_01m6k0c2e4g6j8k0m2p4r6t8vx",
"type": "person.job_changed",
"created_at": "2026-11-01T00:05:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "pchg_01m6j4p2r6t0v4x8z2b6d0f4hm",
"object": "job_change",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"effective_on": "2026-11-01",
"fields": ["grade", "job_title", "position_id"],
"job_title": "Senior operations officer",
"department_id": "dept_01kjsd2v80e5q7m3c9x4ht6wfb",
"manager_id": "person_01kjsf3c00ecxs9b3k1hw7wq2n",
"position_id": "pos_01m6h2k4q8e7r9t1v3w5x7y9za",
"employment_type": "permanent"
}
}
}hr_document.expiring
A standard HR document (a contract, licence or certificate, never an identity, disciplinary or health document) reaches one of its type's reminder days: by default 90, 60, 30 and 7 days before it expires, and the day itself. It fires once per step, at 8 in the morning in your organisation's time zone. days_left is how many days remain (0 on the day). Use it to chase renewals in your own HR system; download the file with GET /hr-documents/{document_id}/file and the documents:read scope.
data.object: HR document · Full schema: hr_document.expiring
{
"id": "evt_01m6k3b5d7f9h1k3m5p7r9t1vx",
"type": "hr_document.expiring",
"created_at": "2026-12-01T08:00:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "hrdoc_01m6k2a4c6e8g0j2r4n6q8s0tv",
"object": "hr_document",
"person_id": "person_01kjw4mg80efgb5aqbsqnxhtbh",
"type": { "id": "hrdoctype_01m6k1z8b0d2f4h6k8m0p2r4tw", "name": "Professional licence", "category": "licence" },
"title": "Medical and Dental Council licence",
"version": 2,
"status": "current",
"visibility": "standard",
"issued_on": "2026-01-02",
"expires_on": "2026-12-31",
"content_type": "application/pdf",
"byte_size": 176004,
"days_left": 30
}
}
}Visitors
visitor.erased
A visitor's personal data is erased, through the API, the console or a data subject request. Delete any copy you hold of this visitor's personal data.
data.object: Visitor erasure · Full schema: visitor.erased
{
"id": "evt_01m4n6fb80e6psh8734zwfns0v",
"type": "visitor.erased",
"created_at": "2026-10-11T10:00:00Z",
"organisation_id": "org_01kjpsrza0etzb0a4vf72x8ang",
"livemode": true,
"data": {
"object": {
"id": "visitor_01kn6t5b80egpr6rwh8jds1930",
"erased_at": "2026-10-11T10:00:00Z"
}
}
}Related
Webhooks
Get a signed HTTPS request the moment something happens in Agoo, such as a visitor checking in, a parcel arriving or a roll call starting.
Verify signatures
Check that every webhook really came from Agoo, with complete verification code for TypeScript, Python, PHP and Go, and rotate secrets without downtime.