admin role (or the finance role for read-only coverage and pay listing) unless noted. The most important invariant on this entire surface: every route that touches a pay figure writes its audit row in the same database transaction as the read. A read that was not logged is a read that did not commit. This is not a post-hoc side effect — it is a transactional guarantee.
Pay data
POST /api/v1/admin/pay/upload
Upload pay data for multiple employees from a CSV file. Requires admin role. Validation is all-or-nothing: if any single row contains an error, nothing is written. A partially-imported payroll would silently fall back the bad entries to the blended default rate, which is worse than no import. Fix every reported error and re-upload. Pass?dry_run=true to validate the file and see what would be written without committing anything.
The CSV upload accepts the following columns. Common header name aliases from popular payroll exports are recognised automatically, because payroll systems never agree on column names. Every bad row is reported at once so operators can fix them in one pass.
Query parameters
boolean
default:"false"
When
true, validates the CSV and reports any errors but writes nothing to the database. The response shows how many rows would be created or updated.Response
errors contains a list of human-readable error strings (one per bad row, up to 50), and created, updated are both 0.
Example
POST /api/v1/admin/pay
Add or update a pay record for a single employee. Requires admin role.string
required
Employee’s email address.
number
required
Pay amount. Must be greater than 0.
string
ISO 4217 currency code (3 characters). Defaults to the org’s
base_currency.string
Date this record takes effect (
YYYY-MM-DD). Defaults to today.string
default:"annual"
annual or hourly.Example
GET /api/v1/admin/pay
List pay records. Requires at minimum thefinance role.
By default, actual pay amounts are masked (shown as a placeholder string like "USD ***"). Pass ?reveal=true to decrypt and return actual amounts. Revealing pay data:
- Requires admin role, or finance role with
finance_can_view_raw_paypolicy enabled - Writes one audit event per person revealed, so the question “who has seen my salary data, and when?” is answerable via
GET /api/v1/admin/audit
Query parameters
boolean
default:"false"
Decrypt and return actual pay amounts. Audited per person. Requires admin role (or finance role if
finance_can_view_raw_pay is enabled).string
Filter by email address (partial match).
integer
default:"100"
Maximum records to return. Capped at 500.
integer
default:"0"
Pagination offset.
Response example (masked)
Response example (revealed)
GET /api/v1/admin/pay/export.csv
Export all pay data as a CSV file. Admin only. Heavily audited — writes one audit event per row exported. Returnstext/csv with the columns: employee_email, annual_pay, currency, effective_date, pay_basis.
DELETE /api/v1/admin/pay/
Delete all pay records for an employee. Used for offboarding leavers and processing GDPR/CCPA erasure requests. Requires admin role. Deletes every version of the record (all effective dates).Path parameters
string
required
The employee’s email address. URL-encode the
@ if necessary.Response
GET /api/v1/admin/coverage
Shows how much of the people directory has valid pay data, and how fresh that data is. Requiresfinance role or above.
Response
stale_message is safe to show directly to finance users. When stale is true, estimates for some employees may be based on outdated rates. Upload a fresh payroll CSV or trigger an HRIS sync to resolve this.Configuration
GET /api/v1/admin/config
Returns the current org configuration. Admin only.PATCH /api/v1/admin/config
Updates one or more configuration values. Admin only. Every change is audited with a before/after snapshot of the changed fields. All fields are optional (this is a partial update):number
Payroll loading multiplier applied to raw salary. Default
1.30 (i.e. 30% on-costs). Must be between 0 and 5.integer
Hours per year used to convert annual pay to an hourly rate. Default
2080. Must be between 1 and 8760.number
Org-wide blended annual pay used for employees with no individual record.
0 means uncosted (those people contribute nothing to totals). Must be 0 or greater.string
Default currency for the org (ISO 4217, 3 characters). Used when a pay record has no explicit currency.
boolean
When
true, authorised callers can see individual per-person costs in responses. Default false. Enabling this has privacy implications — it is audited and flagged in the admin UI.integer
Minimum headcount below which per-person breakdowns are suppressed even when
per_person_breakdown_enabled is true. Default 5. Range 2–100.boolean
When
false (the default), only organisers see cost totals. Participants see headcount and duration only.boolean
When
true, users with the finance role may call GET /api/v1/admin/pay?reveal=true. Default false.boolean
When
true, totals for small meetings are rounded to two significant figures (total_is_approximate: true). Default true.number
Cost above which a
cost_above_threshold nudge is emitted. Default 500.integer
Headcount above which a headcount nudge is emitted. Default
8.integer
Duration above which a duration nudge is emitted. Default
60.integer
Age (in days) after which pay data is considered stale. Affects the
stale flag in coverage reports. Default 180. Minimum 1.Example
Response
Role management
GET /api/v1/admin/roles
Lists all role assignments. Admin only.PUT /api/v1/admin/roles
Assign or update a role. Admin only. Audited.string
required
The user’s email address.
string
required
One of
participant, organiser, team_lead, finance, admin.string
Team identifier for
team_lead assignments. Scopes the team_lead’s reporting access.DELETE /api/v1/admin/roles/
Revokes the role assignment for the specified email. The user reverts toparticipant. Admin only. Audited. Returns 404 if no role row exists.
Audit log
GET /api/v1/admin/audit
Query the audit log. Admin only.string
Filter by the target of the action. Pass an employee email to answer: “who has accessed this person’s pay data, and when?”
string
Filter by the actor (the person who performed the action).
string
Filter by action type, e.g.
PAY_VIEW, PAY_EXPORT, CONFIG_UPDATE, ROLE_GRANT.integer
default:"30"
Look back this many days. Range 1–3650.
integer
default:"200"
Maximum events to return. Capped at 1000.
Response example
Directory
GET /api/v1/admin/directory
Lists people in the directory, ordered by email. Admin only. The directory is populated by the directory sync adapters and is read-only via this endpoint.Query parameters
integer
default:"100"
Maximum records to return. Capped at 1000.
Response example
Adapters and sync
GET /api/v1/admin/adapters
Returns which provider adapters have credentials configured. An adapter with no credentials simply does nothing — it is not an error. Admin only.POST /api/v1/admin/sync/directory
Triggers a directory sync immediately rather than waiting for the scheduled run. Admin only.string
Sync only this provider (
google, microsoft). Omit to sync all configured providers.412 Precondition Failed if no directory provider is configured.
POST /api/v1/admin/sync/hris
Pulls pay data from the configured HRIS integration. Admin only. Returns412 Precondition Failed if MEETCOST_HRIS_ADAPTER is not set.