Changelog
Every change to the Agoo API, webhooks and developer tools, newest first.
This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.
This page records every change to the developer platform: the REST API, webhooks, OAuth and the developer tools. The newest entry is at the top.
2026-10-20 — People and HR: documents
Added
- HR documents (
hrdoc_…):GET /people/{person_id}/documents(the latest version of each, or every version withversions=true),POST /people/{person_id}/documents(the file as the request body, PDF, JPEG, PNG or WebP up to 20 MB, withtype_id,title,issued_on,expires_on,notes,replacesandfile_namein the query; send anIdempotency-Key),GET /hr-documents/{document_id}andGET /hr-documents/{document_id}/file. Standard documents only: identity, disciplinary and health documents never leave Agoo through the API. A new document ischeckinguntil its virus check passes; its file can't be downloaded until then. - Scopes:
documents:readanddocuments:write. - Event:
hr_document.expiring, at each of the document type's reminder steps, withdays_left. - A document type, title or file name that names a Ghana Card is refused with
validation_failed(L.I. 2523).
Nothing changes for existing integrations.
How changes are announced
- Every change is listed here, with its date and what you need to do, if anything.
- During the preview, names and fields may still change before general availability. Changes that could break an integration are marked Breaking and explained, with the reason and how to update your code.
- After general availability,
/v1only changes in backwards-compatible ways: new endpoints, new optional fields, new enum values and new event types can appear at any time, so ignore anything you don't recognise. Anything that would break existing code waits for a new major version, which runs alongside the old one for at least 12 months. See versioning. - Deprecations are announced here before anything is removed, with the date it stops working.
Each entry uses these labels:
| Label | Means |
|---|---|
| Added | Something new. Nothing you need to do. |
| Changed | Existing behaviour changed in a backwards-compatible way. |
| Deprecated | Still works, but will be removed. The entry says when and what to use instead. |
| Removed | No longer available. |
| Breaking | Preview only: a change existing code may need to adapt to. |
| Fixed | The behaviour now matches the documentation. |
2026-10-20 — ID documents and Ghana Cards (L.I. 2523)
Ghana's National Identity Register (Amendment) Regulations, 2026 (L.I. 2523) forbid relying on a photocopy, scan or image of a Ghana Card to check who someone is. Agoo records a Ghana Card's number and never keeps an image of one.
Breaking
- No image with a Ghana Card. An
id_scananswer whosedocument_typeisghana_card, or whosenumberhas the Ghana Card's form (GHA-123456789-0, however it's spaced or hyphenated, in any case), is refused withvalidation_failedatbody.custom_fields.<key>.imageif it carries animage, onPOST /visitsandPATCH /visits/{visit_id}. Send the number alone. A number in the Ghana Card's form is recorded as a Ghana Card whateverdocument_typesays. - ID document answers are always protected.
id_scanfields are always sensitive and masked, socustom_fieldsleaves their answers out when you call with an API key or as a connected app, and events and webhooks never include them. You can still write them. Before, an API key could read backdocument_type,last4andimage. - ID images can't be opened with an API key or by a connected app. They open only for Owners, Admins and Security leads in Agoo's own apps. You can still upload one (
kindid_image) for a passport or another ID.
Added
id_document_typeonPOST /filesfor anid_image: the document the photo shows (passport,drivers_licence,other).ghana_cardis refused withvalidation_failedatbody.id_document_type, so a photo of a Ghana Card is turned away before anything is uploaded.
Changed
- The kiosk and the apps never photograph a Ghana Card: they read its number on the device and keep no picture, or have it typed. Passports and other IDs follow the organisation's choice of scan, photo or typed.
- ID images are kept for 1 day by default and 30 days at most.
2026-10-20 — People and HR: departments, positions, HR profiles, employment history and job changes
Added
- Departments (
dept_…):GET /departments(witharchived=truefor archived ones too),GET /departments/{department_id},POST /departmentsandPATCH /departments/{department_id}(rename, move under another, set the head, code or cost centre, or archive one nobody is in). Reading needspeople:read, writinghr:write. A person'sdepartmentstring follows its department's name. - Positions (
pos_…):GET /positions(filter bydepartment_idorstatus,vacantfor the vacancies),GET /positions/{position_id},POST /positionsandPATCH /positions/{position_id}. Your organisation's plan must include positions (Growth and up); otherwise writes are refused withplan_required. - HR profiles:
GET /people/{person_id}/hr-profileandPATCH /people/{person_id}/hr-profilewith the new scopeshr:readandhr:write. Statutory numbers come by kind and last four only; full numbers, pay accounts, contacts, marital status and a declared disability never leave Agoo through the API. - Employment history (
jobh_…):GET /people/{person_id}/employment-history, newest first by the date each change took effect, paged withlimitandcursor. - Job changes (
pchg_…):GET /people/{person_id}/job-changesandPOST /people/{person_id}/job-changesto change a job from a date: applied at once when it's today or earlier, otherwise on its date. Send anIdempotency-Key. - Events:
department.created,department.updated,position.created,position.updatedandperson.job_changed.
Nothing changes for existing integrations.
2026-10-19 — Meet, Teams and Zoom links on bookings
Added
- How a booking type's video meeting is made:
video_provideron booking types:link(the type'svideo_url, else the host's own link, as before),google_meet,teamsorzoom. Set it when you create or update a booking type. The three providers need Starter or above; on Free they're refused withvalidation_failedonvideo_provider. Existing booking types arelink. - The meeting on each booking:
meeting_provider(google_meet,teams,zoom, ornullfor a pasted link or the host's own) andmeeting_state(none,pending,readyorfailed). Agoo makes the meeting through the host's connected account when a video booking is confirmed;meeting_urlisnullwhile it'spending. A host who hasn't connected the provider gets their own link, withmeeting_providernull.
Nothing changes for existing integrations: both booking fields are new, and booking events carry them. No event announces a link that's made after a booking event was sent yet; get the booking to read it.
2026-10-16 — The deliveries log: richer deliveries, corrections, sending back and two events
Added
- Who a delivery is for:
recipient_type(person,teamorfront_desk) andteam_id. A delivery for a team tells every member, and any of them can collect it; one for the front desk tells nobody.recipient_idisnullunless it's for a person.GET /deliveriestakesteam_idtoo. - More about each delivery:
for(who the label or courier said it's for, in words),kind(parcel,envelope,foodorother),size(small,medium,largeornull),location(where reception keeps it),rider_nameandrider_phone,source(api,console,workspace,kiosk,web_kioskordrop_off),notified_atandreminders_sent,collection_method(code,signature,stafforapi),collected_by(a person ID, when it's known) andhanded_over_by,returned_atandreturn_reason, and its photos,label_file_idandphoto_file_ids(up to four). - Statuses:
notified(waiting, and the recipient has been told),returned(sent back to the sender) andrefused(turned away at the door), besidereceivedandcollected.GET /deliveries?status=waitinglistsreceivedandnotifiedtogether. - Get, correct and send back:
GET /deliveries/{delivery_id}returns one delivery.PATCH /deliveries/{delivery_id}corrects a waiting one (who it's for, where it's kept, what it is); a new recipient gets a new collection code and is told unlessnotify_recipientisfalse.POST /deliveries/{delivery_id}/returnrecords it as sent back, or withrefused: trueas turned away, with an optionalreason. Both return the delivery and take anIdempotency-Key; a delivery that's no longer waiting returnsinvalid_state. - Collecting:
POST /deliveries/{delivery_id}/collecttakesmethod(apiby default;codewith the recipient's six-digitcode,signaturewith the strokes of a drawnsignature, orstaffwith anotesaying why),collected_by(a person ID) andcollected_by_name, as your organisation's rule for collecting allows. A wrong code returnsvalidation_failed. - Creating:
POST /deliveriestakes all of the above. Leave outrecipient_idandteam_idand the delivery goes to your organisation's default recipient. Upload photos first withPOST /files, kinddelivery_label(new: the label) ordelivery_photo(the parcel). Withnotify_recipient(the default) the recipient is told with their collection code and reminded until it's collected, as your organisation's settings say. - Two events:
delivery.updated(a waiting delivery corrected) anddelivery.returned(sent back or refused), with the delivery asdata.object. Endpoints subscribed to*start receiving them the first time a parcel is corrected or sent back; ignore them if you don't need them.
Changed
delivery.receivedanddelivery.collectedcarry the richer delivery. A delivery logged for a person is usuallynotifiedby the time you fetch it.
The collection code itself is never in the API or an event: only the person or team a parcel is for ever sees it. The deliveries endpoints are on Pro and Enterprise in live mode, and every plan in test mode, as before; webhooks are on Growth and above. Nothing is removed or renamed, so the API stays at /v1.
Nothing you need to do: ignore fields, statuses and events you don't recognise. See webhook events and the API reference.
2026-10-15 — Agoo Reviews in the API, and review webhooks
Added
- Three scopes:
reviews:read(your page's reviews, scores and review invitations on Agoo Reviews),reviews:write(reply to reviews and flag them for a moderator) andreviews:invite(invite customers to review and make after-sale review links). They match the Agoo Reviews permissions in the Console; existing keys don't have them. - Reviews (
review_…):GET /reviewslists your page's published reviews, newest first, filtered byplace_id,stars,label,published_afterandunanswered;GET /reviews/{review_id}gets one. Each has itsplace,status,stars,title,body,visit_month,language,label,proof_seen,reviewer_name, yourreply, itsurl,published_atandedited_at. A review never carries the reviewer's contact details. - Scores:
GET /review-scoresreturns your page's score and every branch's, with how many reviews each counts, whether the score is shown yet (from three reviews), and how the stars and labels are spread. - Reply and flag:
POST /reviews/{review_id}/replywith abodypublishes your reply, or changes it, after the same checks as reviews.POST /reviews/{review_id}/flagwith areason(one of the published reasons) and an optionalnotereturns201with the flag; a flag never hides a review. - Review invitations (
rinv_…):GET /review-invitationslists your invitations with their state (never which review came of one);POST /review-invitationsinvites up to 500 customers (name,emailorphone) for a branch (place_id), withdeclared: trueto confirm you invite customers whatever their experience, and returns the batch with how many were invited and why any were left out; send anIdempotency-Key.POST /review-invitations/linkswithplace_idandcountmakes up to 500 single-use after-sale links, valid 30 days. Both return201. - Four events:
review.published,review.updated,review.removedandreview.replied, with the review asdata.object. Endpoints subscribed to*start receiving them when your page gets its first review after this change; ignore them if you don't need them.
All eight operations come with Reviews Business: Growth, Pro and Enterprise in live mode, every plan in test mode; an organisation without it gets plan_required. There is no way to hide, edit, reorder or delete a review through the API. Nothing is removed or renamed, so the API stays at /v1.
Nothing you need to do. See webhook events and authentication.
2026-10-15 — Payroll exports in the API, and the payroll export webhook
Added
- The scope
attendance:payroll: payroll exports: make them and download their files, with every kind of leave by its pay code. Payroll files name private kinds of leave, such as sick and maternity leave, which every other scope shows asaway, so they have a scope of their own. It matches the payroll permission Owners, Admins and HR hold in the Console; a key or app needs it granted explicitly, and existing keys don't have it. - Payroll exports (
payroll_export_…), a new resource.GET /payroll-exportslists your organisation's payroll exports, newest first, filtered byperiod_id;GET /payroll-exports/{payroll_export_id}gets one. Each has itsnumber, itsperiod(id,starts_on,ends_on,payroll_on),site_ids(nullfor the whole organisation),format(csvorxlsx),layout(long,wideorcustom), how manypeople,linesandcorrectionsit holds,superseded(how many of its people a later file has corrected), itsfile(name,content_type,byte_size,sha256),created_by(a person ID, ornullwhen your system made it),created_via(personorapi) andcreated_at. - Make a payroll export:
POST /payroll-exportswithperiod_id, and optionallysite_ids(up to 200),format(csvby default, orxlsx),layout(longby default, orwide) and anote(up to 300 characters); send anIdempotency-Key. It returns201with the export. Only approved, locked timesheets go in; people whose figures changed since an earlier file for the same period get correction lines, and people with nothing new aren't repeated. It returnsinvalid_statewhile the period is still running, while a leave type in it has no pay code or two pay codes clash, and when nothing has changed since the last file for the period. - Download the file:
GET /payroll-exports/{payroll_export_id}/filereturns the file as it was made, matching itssha256, or, withformatorlayout, a copy in another format or layout made from the same lines. - The event
payroll_export.created, with the payroll export asdata.object, whenever a payroll file is made, in the Console or through the API. It carries the export's details, never its lines. Endpoints subscribed to*start receiving it when your organisation makes its first payroll file; ignore it if you don't need it.
All four operations need attendance:payroll and the payroll export, which is on Pro and Enterprise: an organisation on another plan gets plan_required, in test mode too. The webhook is on Growth and above, so it fires only for organisations whose plan has the payroll export. Pay codes and your own file layouts are set in the Console. Nothing is removed or renamed, so the API stays at /v1.
Nothing you need to do. See webhook events and HR and payroll.
2026-10-15 — Timesheets in the API, and timesheet webhooks
Added
- Timesheet periods:
GET /timesheet-periodslists your organisation's periods (tsp_…), filtered byfromandto, each with its dates,kind(weekly,fortnightly,monthly),label(such as "September 2026"),payroll_on,status(open,closed,approved) and how many of its timesheets are open, to confirm, to approve and approved;GET /timesheet-periods/{period_id}gets one. - Timesheets:
GET /timesheetslists timesheets (ts_…), filtered byperiod_id,person_idandstatus(open,to_confirm,to_approve,approved);GET /timesheets/{timesheet_id}gets one with each day of its period. A timesheet carries its period,round, when it was confirmed, submitted and approved, by whom and how (approved_via:person,apiorauto), whether it'slocked, the last reopening and its reason,changed_after_lock, the period'stotalsand where itsapprovalstands. - Approve and reopen:
POST /timesheets/{timesheet_id}/approvefinishes every remaining step at once (approved_via: api) and locks the timesheet; a timesheet still waiting for the person to confirm is sent for approval first. One with something left to fix (a missing clock-out, a break not ended, extra time or leave waiting for a decision, a clock-in waiting for review) returnsinvalid_state, saying what's left.POST /timesheets/{timesheet_id}/reopentakes areason(3 to 500 characters) and sends it back to its first approval step. - The events
timesheet.submitted,timesheet.approvedandtimesheet.reopened, with the timesheet asdata.object. Endpoints subscribed to*start receiving them at the end of your organisation's first period; ignore them if you don't need them.
Changed
- Locked days. Once a person's timesheet is approved, changes to their days in that period are refused with
invalid_stateuntil it's reopened: recording, deciding or cancelling leave, and reviewing a punch. Punches you send withPOST /attendance/eventsare never refused: one on a locked day is kept, not counted until the timesheet is reopened, and the timesheet'schanged_after_lockbecomestrue. No new error code.
Reads need attendance:read; approving and reopening need attendance:manage. The API is on Pro and above and the webhooks on Growth and above, in live mode; every plan in test mode. Private kinds of leave appear in totals.leave as away, as everywhere else in the API.
Nothing you need to do. See webhook events.
2026-10-15 — Rosters, swaps and open shifts in the API, and roster webhooks
Added
- Roster shifts:
GET /roster-shiftslists the shifts on your rosters (roster_shift_…), filtered by site, person and dates;GET /roster-shifts/{roster_shift_id}gets one. Each has its site, person (nullfor an open shift), the day it belongs to (a night shift belongs to the evening it starts), its start and end as UTC instants and as local times at the site, its name, label, break and note. - Plan from your own system:
POST /roster-shiftsadds a shift, for a person or open;PATCH /roster-shifts/{roster_shift_id}changes one (its times, person or break, or makes it open);DELETE /roster-shifts/{roster_shift_id}removes one. Like a manager's changes in the Console, they wait as drafts until the site's roster is published:POST /rosters/publishpublishes everything waiting at a site and tells each person once. Publishing is refused withconflictwhile two of someone's shifts would overlap, and withvalidation_failedwhen a change to a shift starting within 48 hours has nonote. - Swaps:
GET /shift-swapslists swaps (shift_swap_…) between your staff, with the shift, any shift asked for in return, who offered it, who it was offered to, who took it and where its approval stands;GET /shift-swaps/{shift_swap_id}gets one;POST /shift-swaps/{shift_swap_id}/decide(approveordecline, with anoteboth people see, needed to decline) decides a swap waiting for approval, whichever step it's on. Approving moves the shift at once, and is refused withconflictif the person taking it now has another shift at that time. - Claims for open shifts:
GET /shift-claimslists claims (shift_claim_…);GET /shift-claims/{shift_claim_id}gets one;POST /shift-claims/{shift_claim_id}/decidedecides a waiting claim the same way. Approving one gives the shift to that person and closes the other claims on it astaken. - The events
roster.published,shift_swap.requested,shift_swap.approved,shift_swap.declined,shift_claim.requested,shift_claim.approvedandshift_claim.declined. Endpoints subscribed to*start receiving them once your organisation publishes a roster or someone swaps or claims a shift; ignore them if you don't need them.
Reads need attendance:read; adding, changing, removing, publishing and deciding need attendance:manage. There are no new scopes. Rosters come with Growth and above; the API is on Pro and above and the webhooks on Growth and above, in live mode; every plan in test mode. Roster patterns and rotations stay in the Console for now.
The Labour Act consent warnings for night work and overtime, and the records of consent, are never in the API or webhooks.
Nothing you need to do. See webhook events.
2026-10-14 — Leave in the API, and leave webhooks
Added
- Leave types:
GET /leave-typeslists your organisation's leave types (ltype_…), with each one's kind, unit, allowance, whether it's paid, whether it needs approval and whether it's private. - Leave requests:
GET /leave-requestslists leave (leave_…), filtered byperson_id,status(pending,approved,declined,cancelled),type_id,fromandto;GET /leave-requests/{leave_id}gets one, with where its approval stands (approval: its status, the step it's on and how many there are). - Record leave for someone:
POST /leave-requestsrecords leave from your own HR system, approved at once, as HR does in the Console. Agoo counts the days by the person's working days, holidays and closed days, and refuses leave that overlaps theirs or goes beyond their balance. - Decide and cancel:
POST /leave-requests/{leave_id}/decide(approveordecline, with anotethe person sees, needed to decline) decides a waiting request: a decision through the API finishes its approval, whichever step it's on;POST /leave-requests/{leave_id}/cancelcancels a waiting request or approved leave that hasn't started, and the days go back to the balance. - Balances:
GET /people/{person_id}/leave-balancesreturns a person's balance for each kind of leave counted by the year (private kinds left out) this leave year: entitled, carried over, taken, adjusted, expired, booked, waiting and what's left. - The events
leave.requested,leave.approved,leave.declinedandleave.cancelled, with the leave request asdata.object. Endpoints subscribed to*start receiving them once someone asks for leave; ignore them if you don't need them.
Reads need attendance:read; recording, deciding and cancelling need attendance:manage. The API is on Pro and above and the webhooks on Growth and above, in live mode; every plan in test mode.
Private kinds of leave stay private. For a type your organisation marks private (sick and maternity leave by default), the API and webhooks give kind as away, type_name as "Away" and no type_id, reason or decision_note, so an integration never learns what the leave is, and a person's balances leave private types out. Certificates are never in the API or webhooks.
Nothing you need to do. See webhook events.
2026-10-14 — Leave counted in working days
Changed
on_leaveinGET /attendance/summarycounts someone on leave only on the days that leave covers. Leave counted in working days (annual leave, by default) no longer covers a weekend or another day the person doesn't work, so on such a day they aren't counted as on leave. Calendar-day leave (maternity, by default) still covers every day. Sickness recorded during annual leave counts as leave, as before.
Nothing you need to do. Leave types, balances and requests aren't in the API yet; they come with a later update.
2026-10-14 — Break punches
Added
- Break punches. Where an organisation switches them on, people punch Start break and End break:
AttendanceEvent.kindcan bebreak_startorbreak_end, withbreak_typerestornursing. A break sits inside a clock-in and never ends it. break_punchesinGETandPATCH /attendance/settings, and in a site'sGETandPATCH /sites/{site_id}/attendance-settings(nullfollows the organisation), witheffective.break_punches. Off by default.POST /attendance/eventsrecordsbreak_startandbreak_endfrom your terminal where break punches are on at the site, with optionalbreak_type.invalid_statewhen the person isn't clocked in, is already on a break, or isn't on one to end.include_breaksonGET /attendance/events, andkind=break_startorkind=break_end.- The events
attendance.break_startedandattendance.break_ended. Endpoints subscribed to*start receiving them once someone punches a break; ignore them if you don't need them.
Nothing you need to do: GET /attendance/events keeps returning only clock-ins and clock-outs unless you ask for breaks. Worked time in timesheet exports is net of the unpaid breaks people punch, as it is of the automatic one. See webhook events.
2026-10-14 — A shift's own break
Added
break_kindandbreak_minutesonShift(GET /shifts). A shift can carry its own break:paid(counted as worked),unpaid(taken off worked time once the day's work reaches your organisation's threshold; a break the person clocks out for replaces it) ornone, with its length in minutes. Both arenullfor a shift that follows your organisation's standard day, which is every shift until you set one.
Nothing you need to do. Worked time in your timesheet exports is now net of unpaid breaks where your organisation runs a day in two parts; on Ghana's defaults (a paid 30-minute break) nothing comes off.
2026-10-14 — Absence after the shift, and attendance.absent
Changed
GET /attendance/summarycounts someone absent only once their rostered shift has ended with no clock-in, and never on approved leave or a public holiday your organisation observes. Before, it counted them absent once the shift's start plus grace had passed. Absences a manager authorised in the Console still count as absent.
Added
not_in_yetin each day's counts and the totals ofGET /attendance/summary: rostered, past the shift's start plus grace, the shift still running, and no clock-in yet.- The event
attendance.absent. Once per person and day, a few minutes after a rostered shift ends with no clock-in:person_id,date,site_id,shift_id,shift_starts_atandshift_ends_at. Endpoints subscribed to*start receiving it; ignore it if you don't need it.
Nothing you need to do, unless your code reads absent during the working day: those people are now in not_in_yet. See webhook events.
2026-10-14 — Punches a manager added or corrected
Added
- The method
manual. A manager can now add a clock-in or clock-out someone missed, or correct a punch's time, in the Console, always with a reason. Those punches havemethodmanualinGET /attendance/events(and as amethodfilter) and inattendance.clocked_in,attendance.clocked_outandattendance.late, like any other punch, so your payroll sees them. AttendanceEvent.added. Onmanualpunches only:by(the manager's person ID), theirreason, andreplaces, the punch a correction replaced (nullfor a missed punch).recorded_atis when it was added;occurred_atis the time it stands for.- The event
attendance.voided. A punch stopped counting: voided by a manager, rejected after a failed face match, or replaced by a corrected time. Its object carriesvoided(at,by,reason,replaced_by). A correction is never an edit: you getattendance.voidedfor the wrong punch andattendance.clocked_inorattendance.clocked_outfor the right one. Endpoints subscribed to*start receiving it; ignore it if you don't keep punches.
Nothing you need to do unless your code assumes the methods it knows. See webhook events.
2026-10-14 — Line managers and employment details on people
Added
- Five fields on
Person:manager_id(the ID of the person's line manager, ornull),started_on(their first day, a date, ornull),left_on(the day they left, a date, ornull),employment_type(permanent,temporary,casualorpart_time) andcontracted_hours(hours a week, from 0.5 to 80 with one decimal, ornull). They're always in the response.left_onis set to the day someone is deactivated if it's empty, and cleared if they're reactivated. - Setting them.
POST /peopletakesmanager_id,started_on,employment_typeandcontracted_hours;PATCH /people/{person_id}takes those andleft_on, andnullclears any of them exceptemployment_type(which defaults topermanent). manager_idonGET /peoplelists only the people whose line manager is that person (their direct reports).
A manager must be an active person in the organisation, and can't be the person themselves or anyone who reports to them, directly or further down: those requests return validation_failed. Nothing you need to do: these are new fields, so existing code keeps working. See Line managers and HR.
2026-10-14 — Kiosks in a web browser
Added
Device.platformcan beweb. An organisation can now set up a front-desk computer as a kiosk in its web browser, beside the Kiosk apps. A site'sdevices(GET /sitesandGET /sites/{site_id}) list it like any paired kiosk, withplatformwebwhere a tablet or phone hasiosorandroid; itsapp_versionis the kiosk page's build. Nothing else about the object changes, and there's nothing you need to do unless your code assumes onlyiosandandroid. See Set up a kiosk on a computer.
2026-10-13 — Icons on visit types
Added
VisitType.icon.GET /visit-typesandGET /visit-types/{visit_type}return each type's icon: the key of one icon in Agoo's set for visit types, such ashandshake,packageorbackpack, which an admin picks in Console → Settings → Forms. Publishable keys get it too, so a pre-registration form can show the same tile as the kiosk. Built-in types start with their own icon and added types withuser. New keys can be added later, so drawuserfor one you don't know. See Icons. Nothing you need to do.
2026-10-12 — Visits for several people, a team, or no one in particular
Added
- A visit can be for more than one person.
Visithasaudience(person,people,teamordeskfor no one in particular),hosts(everyone the visit is for, each with aroleand astate),team,asked_for,needs_someoneandrescheduled_to.POST /visitstakeshost_ids(the first leads) orteam_idinstead ofhost_id; with none of them the visit is for no one in particular. - Teams.
GETandPOST /teams,GET,PATCHandDELETE /teams/{team_id},POST /teams/{team_id}/membersandDELETE /teams/{team_id}/members/{person_id}, with thepeople:readandpeople:writescopes. - Working on a waiting visit.
POST /visits/{visit_id}/hostsandDELETE /visits/{visit_id}/hosts/{person_id},ask,answer,take,hand-overandreschedule, andneeds_someoneandteam_idas filters onGET /visits. - Events
visit.needs_someoneandvisit.host_changed. Person.visitors_viaandPerson.kiosk_listed. Who receives a person's walk-in visitors for them, and whether visitors can find the person at the kiosk and on your visitor pages. Read only: they are set in Console → People.- A visit type's
host.askandoffersay what a visitor may answer to "Who are you here to see?", so a form built with a publishable key knows whether to ask and what to send. With a secret key or an OAuth app,answer,waitandreceiversay who decides and who receives a visitor with no one to see.
Breaking
Visit.host_idcan benull. A visit for a team or for no one in particular has no host until someone takes it. If your code readshost_id, handlenull, or readhostsandteaminstead. Visits for one person are unchanged.
2026-10-09 — API keys, webhooks and test mode in the Console, and vehicle plates
Added
- Keys and endpoints in the Console. Owners and Admins make and revoke API keys in Console → Settings → Developers → API keys, manage webhook endpoints under Webhooks, and read what test mode would have sent in the Test outbox. Test keys work on every plan. See authentication and environments.
vehicle_plateonPOST /visits,PATCH /visits/{visit_id}and the Visit object, and the file kindvehicle_photo.Person.works_gate(2026-10-08): whether the person signs in on a device in guard mode.
2026-10-07 — Photos, files, signatures and ID scans in custom fields
Added
- Capture fields can be answered. A visit form's photo, file, signature and ID scan questions now appear in its JSON Schema with
x-agoo-typephoto,file,signatureorid_scan, andPOST /visitsandPATCH /visits/{visit_id}accept their answers with a secret key. See Custom fields. They're never in a public form. - File kinds
answer_photoandanswer_fileonPOST /files, for the photo and file answers. A file used by an answer belongs to the visit and is deleted with it.
Changed
- ID scan answers read back as
document_type,last4andimage. Thenumberyou send is kept encrypted with the visitor and never returned through the API.
2026-10-06 — Pass rules, multi-day passes and re-entry
Added
pass_rulesonPOST /visitsandPATCH /visits/{visit_id}, andpass.ruleson the Visit object: daily hours (Starter and above), an entry window, single use and allowed gates (gate_ids, Growth and above). Left out on create, a visit takes its visit type's defaults as your plan allows them. Not with a publishable key. A visit's days, fromexpected_attoexpected_until, can now span up to 92 days. See Passes and pass rules.override_pass_rulesonPOST /visits/{visit_id}/check-in. A check-in the pass's rules don't allow now isinvalid_statewith an error atpass_rule.<reason>; the override lets the visitor in and is recorded in the audit trail.POST /visits/{visit_id}/re-enter: a visitor back in on a pass that admits again (another day of a multi-day pass, or back after leaving). It returns a new visit for the entry, withpass.for_visit_idnaming the visit that holds the pass, and firesvisit.createdandvisit.checked_in.
Changed
- Passes are signed. The QR code on a visitor's pass now carries a code signed with the organisation's key, for Agoo's kiosks and guard apps to check offline. Treat it as opaque; nothing in the API reads it.
2026-10-05 — person.reactivated
Added
person.reactivated. Sent when a deactivated person is reactivated in the console, with the same Person object asperson.deactivated. There are now 30 event types. Endpoints subscribed to*receive it; others can add it.
2026-10-02 — Files and attendance evidence
Added
- Files.
POST /files,GET /files/{file_id}andGET /files/{file_id}/download, with the scopesfiles:readandfiles:write. See Files and uploads. - Attendance evidence and settings.
GETandPATCH /attendance/settingsand/sites/{site_id}/attendance-settings,GET /attendance/evidence,GETandPOST /people/{person_id}/attendance-evidence,POST /attendance-evidence/{evidence_id}/withdrawandPOST /attendance/events/{clock_id}/review, with the new scopeattendance:manage. There are now 25 scopes. - Error codes
consent_required(403) andlocation_check_failed(422).draft_conflict(409) followed on 2026-10-06. entered_while_offlineon a watchlist match, when a kiosk let the visitor in before it could know of the match.
2026-10-02 — Form builder and protected answers
Admins now build forms in Console → Settings → Forms, so the schemas behind custom_fields change when they publish.
Added
- Screen order. Form schemas (
GET /forms, andpublic_formon visit types and booking types) list their properties in screen order inx-agoo-order. JSON objects don't keep key order, so use it rather than the order ofproperties. - Field annotations. Properties carry
x-agoo-type(the builder's field type), and where they applyx-agoo-labels(choice labels by value),x-agoo-unit,x-agoo-consent-text,x-agoo-show-if(asked only after an earlier answer) andx-agoo-sites(asked only at those sites). Conditional requirements areallOfif/then rules. - Yes or no questions are
booleanproperties, like consent. A Person question is a string holding a person ID (person_...).
Changed
- Protected answers aren't sent to integrations. Answers to fields an admin marked sensitive or masked are left out of
custom_fieldsfor API keys and connected apps, and out of every event and webhook. Signed-in people see them only in the roles the admin chose. See Custom fields.
2026-10-01 — Public forms for pre-registration and bookings, and plan history
Changes to the preview contract, made before anything ships.
Breaking
- Publishable keys pre-register only for open types.
POST /visitswith a publishable key now needs atypean admin has opened for pre-registration; any other type, including the defaultmeetingwhen it isn't open, returnsvalidation_failedatbody.type. Itscustom_fieldsmay contain only the fields asked of the visitor (those in the type'spublic_form); a field your staff fill in returnsvalidation_failedatbody.custom_fields.<key>. See public forms. - Publishable keys book with the public intake questions only.
POST /bookingswith a publishable key accepts only thecustom_fieldsin the booking type'spublic_form; a field your staff fill in returnsvalidation_failedatbody.custom_fields.<key>.
Added
VisitTypeObjectgainspre_registration(truewhen an admin has opened the type for pre-registration; off by default) andpublic_form({ version, schema }: the type's visit form with only the fields asked of the visitor, ornullunlesspre_registrationistrue).- Publishable keys can call GET/visit-types and GET/visit-types/{visit_type}. They see only types that are turned on and open for pre-registration; any other type is
not_found.GET /formsstays closed to publishable keys. The planned React<PreRegistrationForm>will use this to fetch its types and questions itself. BookingTypegainspublic_form(the same{ version, schema }shape): the intake questions asked of the visitor on itsvisit_type's form, forpublicbooking types;nullforprivateones. Publishing a booking type makes these questions public, whether or not the visit type is open for pre-registration. The planned React<BookingWidget>will render them.
Changed
- Plan history in the API. In live mode, your plan's visitor history decides which visits and visitor records the API shows, and its audit-trail period which audit events. Moving to a lower plan never deletes data: older records are kept, hidden, and come back when you move up. Lists leave them out; reading or acting on one by ID returns
plan_required, notnot_found. Visits that are stillexpected,awaiting_approvalorchecked_inare never hidden, andDELETE /visitors/{visitor_id}covers hidden records too. There's no API export of hidden records; an organisation's Owner or Admin can request a full export, including them, from the console on any plan. <PreRegistrationForm>needshost. Publishable keys never list or search people, so the planned React component takes the host from your page and shows a message instead of the form without one. For host search, use the embed script, whose hosted page has it.
2026-10-01 — Custom visit types, booking confirmation and MCP client registration
Changes to the preview contract, made before anything ships. If you generated a client from the first preview, regenerate it.
Breaking
- Visit types are now per organisation.
VisitTypeis no longer a fixed enum. It's a key: the six built-in keys (meeting,interview,delivery,contractor,event,other) plus the keys of types your organisation adds, such asparent_pickup. Keys are lowercase letters, digits and underscores, start with a letter and are at most 40 characters. This applies totypeon visits and the visits filter,visit_typeon booking types, forms and the forms filter, and every webhook payload that carries them. Treat them as strings. An unknown key, or a type that is turned off, returnsvalidation_failedwhen you create a visit or change its type. See custom fields. - Bookings can wait for the host.
BookingStatusaddspending,declinedandexpiredtoconfirmedandcancelled. A booking made on a booking type that requires confirmation starts aspending, holds its slot, and has novisit_iduntil the host confirms it. Cancel and reschedule work only onconfirmedbookings. Bookings gainconfirm_by,confirmed_at,declined_atanddecline_reason.
Added
- GET/visit-types and
GET/visit-types/{visit_type}, scope
forms:read, to list your organisation's visit types with their names, whether they're turned on, and their forms. - Booking types gain
requires_confirmationandconfirmation_window_hours(default 24). - POST/bookings/{booking_id}/confirm and
POST/bookings/{booking_id}/decline, scope
bookings:write. A decline reason is shown to the attendee only withshare_reason_with_attendee. - Events
booking.confirmed,booking.declinedandbooking.expired, for 29 event types in all.booking.createdstill fires for every booking, with itsstatus. - Client ID Metadata Documents for MCP clients, as the MCP authorization specification (revision 2026-07-28) recommends: the client's
client_idis an HTTPS URL serving its metadata. The server metadata advertisesclient_id_metadata_document_supported: true. See authentication.
Changed
- Dynamic client registration stays available for MCP clients that don't support metadata documents. The MCP specification now marks it deprecated; Agoo keeps it, and any change will be announced here first.
- Plan access for website embeds, stated precisely: the embed script and the WordPress plugin's blocks work on every plan that has booking pages, because they show hosted pages in a frame. The React components call the API directly, so they need Pro or Enterprise in live mode, and work on every plan in test mode.
2026-10-01 — Preview contract published (v1.0.0-preview)
The first public preview of the Agoo developer platform. This contract lets you design and build integrations now.
Added
- REST API v1 at
https://api.agoohq.com/v1, described in an OpenAPI 3.1 document and in the API reference. Resources: organisation, sites, people, visits (including check-in, check-out, approve, deny, cancel and resend pass), visitors (including erasure under Act 843), deliveries, booking types and availability, bookings, attendance events and summaries, shifts, watchlist, roll calls, webhook endpoints, events, audit events and forms. - Authentication with secret keys (
agoo_sk_live_…,agoo_sk_test_…) and browser-safe publishable keys (agoo_pk_live_…,agoo_pk_test_…), each with scopes. 22 scopes, fromorganisation:readtoforms:read. See authentication. - OAuth 2.1 for third-party apps and MCP clients: authorization code with PKCE (
S256), rotating refresh tokens, dynamic client registration, and admin approval per organisation. - Test mode on every plan:
_test_keys work on a sandbox copy of your organisation, with messages held in the console's test outbox. See environments. - Conventions: TypeID identifiers, RFC 3339 timestamps in UTC, E.164 phone numbers, money in minor units, cursor pagination, RFC 9457 problem details with stable error codes,
Idempotency-Keyon everyPOST, per-organisation rate limits withRateLimit-headers, and anAgoo-Request-Idon every response. See concepts. - Custom fields on visits and people, defined per organisation as JSON Schema and readable with
GET /forms. See custom fields. - Webhooks with Standard Webhooks signing, retries for about 27 hours and 26 event types across visits, passes, deliveries, bookings, attendance, the watchlist, roll calls, people and visitors. See webhooks and event types.
- Plan access: webhooks on Growth; the full API, OAuth apps and MCP on Pro; higher limits, custom scopes and IP allow-lists on Enterprise.
- Documentation for planned tools, so you can plan integrations with them: the MCP server, the TypeScript SDK, the webhooks helper, React components, the embed script, the CLI, the WordPress plugin and other languages. Each will be announced here when it's published.