Endpoint catalogue
The conventions (authentication, pagination, errors, idempotency, async jobs) are in the API overview. Permission keys come from the RBAC permission matrix, which is the master list. Module behaviour behind each endpoint is in the module specs, for example Attendance and Leave.
How to read the tables:
- Paths are relative to the internal base
https://<tenant-host>/api/v1. Rows marked Public are also served athttps://api.spyrl.app/v1with the same path. Control-plane paths are relative tohttps://cp.spyrl.app/api/v1. - Permission is the key the guard checks. The scope comes from the caller's grant (SELF, TEAM, DEPARTMENT, LOCATION, ORG); list endpoints are filtered to that scope automatically. "Authenticated" means any signed-in user, acting on their own data.
- Approvals for every request type are decided through
/ntf/tasks. The guard checks that the caller is the task's assignee (or a delegate) and holds the subject's approve permission, for examplelve.request.approvefor a leave task. GET / POSTon one row means "list" and "create" on the same collection.PATCHupdates useIf-Match. MutatingPOSTs take anIdempotency-Key(required where the overview says so).- Decision pending marks a gap in the permission matrix that needs a new key or a decision.
1. PLT: platform and tenant administration
1.1 Tenant-facing (app API)
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /plt/subscription | Plan, add-ons, usage this month, invoices | plt.subscription.read | |
| POST | /plt/subscription/changes | Change plan or add-ons | plt.subscription.manage | Proxied to the control plane |
| GET | /plt/entitlements | Effective features and limits for this tenant | Authenticated | Drives navigation in web and mobile |
| GET | /plt/support-access | Support access requests and the access log | plt.support_access.approve | Visible to the tenant by design |
| POST | /plt/support-access/:id/approve | Approve a support session | plt.support_access.approve | MFA step-up |
| POST | /plt/support-access/:id/deny | Deny a request | plt.support_access.approve | |
| POST | /plt/support-access/:id/revoke | End an active support session | plt.support_access.approve | |
| POST | /plt/data-exports | Request a full tenant export (DPDP portability, offboarding) | plt.data_export.request | Job resource |
| POST | /plt/closure-requests | Request account closure and deletion | plt.tenant.close_request | Cooling-off period applies |
1.2 Public (unauthenticated)
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /public/tenants/resolve?code= | Company code → tenant host, name and branding (mobile first run) | None | Rate-limited per IP; reveals nothing for unknown codes beyond 404 |
| GET | /public/lms/certificates/:no/verify | Verify a certificate number | None | Returns name, course and date only |
| GET | /org/branding | Logo and colours for the login page | None | On the tenant host; cacheable |
1.3 Control plane (https://cp.spyrl.app/api/v1)
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| POST | /signups | Self-serve trial sign-up | None (captcha, email verification) | Starts provisioning |
| GET / POST | /tenants | Search tenants; create and provision a tenant | plt.tenant.read / plt.tenant.manage | POST returns 202 with a provisioning run (see §18.6) |
| GET | /tenants/:id | Tenant detail: plan, cell, status, health | plt.tenant.read | |
| PATCH | /tenants/:id | Edit display name, trial end, notes | plt.tenant.manage | |
| GET | /tenants/:id/provisioning-runs/:runId | Provisioning progress by step | plt.tenant.read | |
| POST | /tenants/:id/provisioning-runs/:runId/retry | Resume a failed run from the failed step | plt.tenant.manage | Steps are idempotent |
| POST | /tenants/:id/suspend | Suspend (read-only) | plt.tenant.manage | Never during the payroll window without a second approver |
| POST | /tenants/:id/reactivate | Reactivate | plt.tenant.manage | |
| POST | /tenants/:id/moves | Move a tenant to another cell | plt.tenant.manage | Second approver; job resource |
| POST | /tenants/:id/exports | Export a tenant's data | plt.tenant.manage | Job resource |
| POST | /tenants/:id/deletion | Schedule deletion after the export window | plt.tenant.manage | Second approver; blocked by legal hold |
| GET / POST | /tenants/:id/domains | List or add a custom domain | plt.tenant.manage | Enterprise |
| POST | /tenants/:id/domains/:domainId/verify | Check the TXT record and issue the certificate | plt.tenant.manage | |
| PUT | /tenants/:id/subscription | Set plan and add-ons | plt.plan.manage | |
| PUT | /tenants/:id/entitlement-overrides | Time-boxed limit overrides | plt.plan.manage | Reason required |
| GET | /tenants/:id/usage | Daily usage metrics | plt.tenant.read | |
| GET / PUT | /plans, /plans/:id | Plans and entitlements | plt.plan.manage | |
| GET / POST | /invoices | Invoices; create a manual invoice or credit note | plt.invoice.manage | |
| GET / PUT | /flags, /flags/:key | Flags and targeting rules | plt.flag.manage | |
| POST | /flags/:key/kill | Toggle a documented kill switch | plt.flag.kill_switch | Incident use |
| GET / POST | /cells | Cell registry; create a cell | plt.cell.manage | |
| PATCH | /cells/:id | Resize, maintenance window, status | plt.cell.manage | |
| POST | /releases/:id/rollouts | Start a wave rollout | plt.release.rollout | |
| POST | /rollouts/:id/pause / resume / abort | Control a rollout | plt.release.rollout | |
| PUT | /mobile/min-version | Minimum supported app version per platform | plt.release.rollout | Drives 426 |
| POST | /support-sessions | Request access to a tenant | plt.support.request | Reason and ticket required |
| POST | /support-sessions/:id/emergency-approve | Emergency access without tenant consent | plt.support.emergency_approve | Second person |
| POST | /statutory-packs | Draft a statutory pack version | plt.statutory_pack.publish | |
| POST | /statutory-packs/:id/publish | Publish to all cells | plt.statutory_pack.publish | Emits plt.statutory_pack.published |
| POST | /announcements | Platform banner for a plan, cell or tenant | plt.announcement.publish | |
| GET | /audit-events | Platform audit log | plt.audit.read | |
| GET / POST | /platform-users | Manage Spyrl staff accounts | plt.platform_user.manage | |
| POST | /billing/razorpay/webhook | Razorpay subscription events | Razorpay signature | Idempotent on the event ID |
2. Kernel (sys)
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| POST | /sys/files | Register a file and get a presigned upload URL | Authenticated (the purpose decides who may attach it) | Public |
| POST | /sys/files/:id/complete | Confirm the upload and start scanning | Uploader | Public |
| GET | /sys/files/:id/download | Redirect to a short-lived download URL | Read permission on the owning entity | Public; audited for C3/C4 files |
| GET | /sys/jobs/:id | Status of an async job | Requester, or the job's admin permission | Public |
| POST | /sys/jobs/:id/cancel | Cancel a job | Requester | |
| GET | /sys/policy-kinds | Policy kinds and schema versions | Any *.policy.manage | |
| GET | /sys/policy-kinds/:kind/schema | JSON Schema for a policy kind (drives the policy editor) | Any *.policy.manage | |
| GET | /openapi.json | The internal OpenAPI 3.1 document | Authenticated | The public spec is at https://api.spyrl.app/v1/openapi.json |
3. ORG: organisation and masters
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /org/company | Company profile | org.company.read | Public |
| PATCH | /org/company | Edit the company profile | org.company.manage | |
| PATCH | /org/branding | Logo, colours, sender name | org.branding.manage | |
| GET | /org/setup | Setup checklist and go-live readiness score | org.company.manage | |
| GET / POST | /org/legal-entities | Legal entities | org.company.read / org.company.manage | Multi-entity tenants: decision pending |
| PATCH | /org/legal-entities/:id | Edit a legal entity | org.company.manage | |
| GET / POST | /org/locations | Locations (state, time zone, ID sequence) | org.company.read / org.location.manage | Public (GET) |
| PATCH | /org/locations/:id | Edit a location | org.location.manage | Emits org.location.updated |
| GET / POST | /org/locations/:id/geofences | Geo-fences and allowed networks | att.geofence.manage | |
| PATCH | /org/locations/:id/geofences/:fenceId | Edit a fence | att.geofence.manage | Emits org.geofence.changed |
| PUT | /org/locations/:id/calendar | Assign a holiday calendar from a date | org.holiday.manage | Effective-dated |
| GET / POST | /org/departments | Departments | org.company.read / org.department.manage | Public (GET) |
| PATCH | /org/departments/:id | Edit a department | org.department.manage | |
| POST | /org/departments/:id/deactivate | Deactivate (blocked while employees are assigned) | org.department.manage | |
| PUT | /org/departments/:id/designations | Allowed designations for a department | org.designation.manage | |
| GET / POST | /org/cost-centers | Cost centres | org.department.manage | |
| GET / POST | /org/designations | Designations | org.company.read / org.designation.manage | Public (GET) |
| PATCH | /org/designations/:id | Edit or deactivate | org.designation.manage | |
| GET / POST | /org/grades | Grades | org.designation.manage | |
| GET / POST | /org/holiday-calendars | Holiday calendars | org.holiday.read / org.holiday.manage | Public (GET) |
| POST | /org/holiday-calendars/:id/holidays | Add holidays (bulk) | org.holiday.manage | Emits org.holiday.changed |
| DELETE | /org/holiday-calendars/:id/holidays/:holidayId | Remove a holiday | org.holiday.manage | Triggers recompute |
| GET / POST | /org/custom-fields | Custom field definitions | org.custom_field.manage | |
| GET / PATCH | /org/id-sequences | Employee and document number sequences | org.company.manage | |
| GET / POST | /org/bank-accounts | Employer debit accounts for bank files | org.company.manage | Account numbers masked |
| GET | /org/chart | Org chart | org.chart.read | |
| GET / POST | /org/accounts | Shared client accounts (FFM and TSM) | ffm.account.read / ffm.account.manage | Public |
| PATCH | /org/accounts/:id | Edit an account | ffm.account.manage | |
| GET / POST | /org/contacts | Account contacts | ffm.account.read / ffm.account.manage |
4. IAM: identity and access
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| POST | /iam/auth/login | Password login | None | Returns tokens or an MFA challenge |
| POST | /iam/auth/mfa/verify | Complete an MFA challenge or step-up | Challenge token | |
| POST | /iam/auth/refresh | Rotate the refresh token | Refresh cookie or token | Reuse detection |
| POST | /iam/auth/logout | End the session | Authenticated | |
| GET | /iam/auth/sso/:connectionId/start | Start OIDC or SAML login | None | |
| GET / POST | /iam/auth/sso/callback | SSO callback | IdP response | JIT link to existing employees only |
| POST | /iam/auth/password/forgot | Send a reset link | None | Same response whether or not the user exists |
| POST | /iam/auth/password/reset | Set a new password with a reset token | Reset token | |
| POST | /iam/invites/:token/accept | Activate an account from an invite | Invite token | Sets password; MFA enrolment if required |
| GET | /iam/me | Current user, employee and tenant summary | Authenticated | |
| GET | /iam/me/permissions | Effective permissions with scopes | Authenticated | For UI gating |
| POST | /iam/me/password | Change password | Authenticated | |
| POST | /iam/me/mfa/totp | Start TOTP enrolment | Authenticated | Returns the otpauth URI |
| POST | /iam/me/mfa/totp/verify | Confirm enrolment; returns recovery codes | Authenticated | |
| GET | /iam/sessions | List sessions | iam.session.revoke | SELF for employees |
| DELETE | /iam/sessions/:id | Revoke a session | iam.session.revoke | |
| GET | /iam/users | User accounts and status | iam.user.read | |
| POST | /iam/users/:id/invite | Send or resend the activation invite | iam.user.invite | Bulk variant: /iam/users/invites |
| POST | /iam/users/:id/deactivate | Deactivate | iam.user.deactivate | Revokes sessions and devices |
| POST | /iam/users/:id/reactivate | Reactivate | iam.user.deactivate | |
| POST | /iam/users/:id/unlock | Clear a lockout | iam.user.deactivate | |
| GET | /iam/permissions | Permission registry | iam.role.read | |
| GET / POST | /iam/roles | Roles | iam.role.read / iam.role.manage | |
| POST | /iam/roles/:id/clone | Clone a system role | iam.role.manage | |
| PATCH | /iam/roles/:id | Rename or describe a custom role | iam.role.manage | |
| PUT | /iam/roles/:id/permissions | Set grants (permission + max scope) | iam.role.manage | |
| GET / POST | /iam/role-assignments | Assign roles with scope and filters | iam.role.read / iam.role.assign | Only roles whose grants the caller holds |
| DELETE | /iam/role-assignments/:id | Remove an assignment | iam.role.assign | |
| POST | /iam/devices | Register the current mobile device | Authenticated | May become PENDING_APPROVAL |
| GET | /iam/devices | Devices awaiting approval or active | iam.device.approve | |
| POST | /iam/devices/:id/approve | Approve a device | iam.device.approve | |
| POST | /iam/devices/:id/revoke | Revoke a device (remote sign-out) | iam.device.approve | |
| GET / PUT | /iam/policies/:kind | Password, session, MFA and device policies | iam.policy.manage | Typed policy documents |
| GET / POST | /iam/sso-connections | OIDC and SAML connections | iam.sso.manage | |
| PATCH | /iam/sso-connections/:id | Edit or enforce SSO | iam.sso.manage | |
| GET / POST | /iam/api-clients | Public API clients | iam.api_client.manage | Secret shown once |
| POST | /iam/api-clients/:id/rotate-secret | Rotate a client secret | iam.api_client.manage | |
| DELETE | /iam/api-clients/:id | Revoke a client | iam.api_client.manage | |
| GET | /iam/audit-events | Search the audit log | iam.audit.read | |
| POST | /iam/audit-exports | Export the audit log | iam.audit.export | Job resource; MFA step-up |
5. EMP: core HR
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /emp/directory | Directory (name, photo, designation, work contact) | emp.directory.read | Search with q |
| GET / POST | /emp/employees | List employees; add a new hire | emp.profile.read / emp.profile.create | Public |
| GET | /emp/employees/:id | Employee profile (non-sensitive) | emp.profile.read | Public |
| PATCH | /emp/employees/:id | Edit the record directly | emp.profile.update | Public |
| GET | /emp/employees/:id/personal | Personal data, identity numbers masked | emp.profile.read | |
| PATCH | /emp/employees/:id/personal | Edit personal and identity data | emp.sensitive.update | Encrypted fields |
| POST | /emp/employees/:id/personal/reveal | Unmask Aadhaar or PAN for one view | emp.sensitive.read | Audited; MFA step-up |
| GET / POST | /emp/employees/:id/job-assignments | Job history; record a transfer, promotion or manager change from a date | emp.profile.read / emp.job.change | Public; emits emp.job.changed |
| GET / POST | /emp/employees/:id/bank-accounts | Bank accounts | emp.sensitive.read / emp.sensitive.update | Masked by default |
| GET / POST | /emp/employees/:id/family-members | Family and nominees | emp.profile.read / emp.profile.update | Employees use change requests |
| GET / POST | /emp/employees/:id/education | Education | emp.profile.read / emp.profile.update | |
| GET / POST | /emp/employees/:id/experience | Work experience | emp.profile.read / emp.profile.update | |
| POST | /emp/employees/:id/confirm-probation | Confirm or extend probation | emp.probation.confirm | Emits emp.employee.confirmed |
| POST | /emp/employees/:id/preboarding-link | Send the self-onboarding link | emp.onboarding.manage | Single-use, expiring |
| GET / POST | /emp/preboarding/:token | Load and submit the self-onboarding form | Pre-boarding token | Emits emp.preboarding.submitted |
| GET / POST | /emp/documents | Documents; upload | emp.document.read / emp.document.upload | Files via /sys/files |
| POST | /emp/documents/:id/verify | Verify a document | emp.document.verify | |
| POST | /emp/documents/:id/reject | Reject with a reason | emp.document.verify | |
| GET / POST | /emp/checklist-templates | Onboarding and offboarding checklists | emp.onboarding.manage | |
| GET | /emp/tasks | Checklist tasks (mine, my team, all) | emp.onboarding.manage, or task owner | |
| POST | /emp/tasks/:id/complete | Complete a task with evidence | Task owner or emp.onboarding.manage | Exit clearance uses ext.clearance.update |
| GET / POST | /emp/change-requests | Profile change requests | emp.change_request.create | Approved through /ntf/tasks (emp.change_request.approve) |
| GET / POST | /emp/induction-programs | Induction programmes and attendees | emp.onboarding.manage | |
| GET / PUT | /emp/letter-templates | Letter templates | ntf.template.manage | |
| POST | /emp/letters | Generate a letter (appointment, confirmation, relieving) | emp.letter.generate | Job resource for bulk |
| POST | /emp/imports | Upload an import file and run a dry run | emp.import.run | dryRun=true by default |
| GET | /emp/imports/:id | Validation report | emp.import.run | |
| POST | /emp/imports/:id/commit | Commit a validated import | emp.import.run | Idempotent per row |
| GET / POST | /emp/consents | My consents (face, location, notices) | Authenticated | SELF only |
| POST | /emp/consents/:id/withdraw | Withdraw a consent | Authenticated | Triggers template deletion |
6. ATT: attendance
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| POST | /att/punches | Punch in or out (web or mobile, selfie and location) | att.punch.create | Idempotency-Key = clientRef; see §18.1 |
| GET | /att/punches | Punch log by employee and date range | att.record.read | Public; time-range filter required |
| POST | /att/punches/import | Import punches (CSV, Excel, device logs) | att.import.run | Public; dryRun; job resource |
| GET | /att/days | Computed attendance days | att.record.read | Public |
| PATCH | /att/days/:id | Direct correction with a reason | att.record.edit | Audited; blocked in locked periods |
| POST | /att/days/recompute | Recompute a range of employee-days | att.day.recompute | Job resource; refused for locked periods |
| GET | /att/calendar | Month calendar with legend, holidays and leave | att.record.read | employeeId=me |
| GET | /att/roster | Roster view by location and dates | att.record.read | |
| GET / POST | /att/requests | Attendance requests: work type (WFH, OD, remote), regularization, permission, shift change, overtime | By type: att.work_request.request, att.regularization.request, att.permission.request, att.shift_change.request | On-behalf entry needs the matching approve key (att.work_request.approve for work types) |
| PATCH | /att/requests/:id | Edit while submitted | Requester | |
| POST | /att/requests/:id/withdraw | Withdraw | Requester | |
| POST | /att/requests/:id/cancel | Ask to cancel an approved future request | Requester | Starts a cancellation workflow |
| GET / POST | /att/shifts | Shift masters | att.shift.manage | |
| PATCH | /att/shifts/:id | Edit a shift | att.shift.manage | Triggers recompute for open periods |
| GET / POST | /att/rotations | Rotation sequences | att.roster.manage | |
| GET / POST | /att/weekoff-patterns | Week-off patterns | att.roster.manage | |
| GET / POST | /att/shift-assignments | Assign shifts, rotations and week-offs for a date range (bulk) | att.roster.manage | Up to 500 employees per call |
| GET / POST | /att/policies | Attendance and overtime policies | att.policy.manage | |
| GET / POST | /att/policies/:id/versions | Versions; create a draft | att.policy.manage | Validated against the kind's schema |
| POST | /att/policies/:id/versions/:versionId/publish | Publish from a date | att.policy.manage | Conflict check; back-dating into locked periods is blocked |
| GET / POST | /att/policies/:id/assignments | Applicability rules | att.policy.manage | |
| GET | /att/policies/:id/applicability-preview | Who this policy applies to on a date | att.policy.manage | |
| POST | /att/policies/:id/simulate | Simulate recomputed days before publishing | att.policy.manage | Job resource |
| GET / POST | /att/devices | Biometric devices | att.device.manage | |
| PATCH | /att/devices/:id | Edit or rotate the device secret | att.device.manage | |
| GET / PUT | /att/devices/:id/user-maps | Device user ID ↔ employee mapping | att.device.manage | Effective-dated |
| GET / POST | /att/period-locks | Locks; start a lock with pre-lock checks | att.period.lock | Returns blockers |
| POST | /att/period-locks/:id/unlock | Reopen a period (while the pay run is not locked) | att.period.lock | Reason required |
| GET | /att/adjustments | Post-lock adjustments awaiting payroll | att.record.read | |
| POST | ingest:/devices/:serial/punches | Device push (ADMS-style) | att.punch.ingest (device secret) | On ingest.spyrl.app |
| GET | ingest:/devices/:serial/commands | Device polls for pending commands (time sync, user upload) | att.punch.ingest |
7. MOB: mobile and kiosk
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /mob/config | Navigation groups, entitlements, minimum version, cached fences | Authenticated | Called at app start |
| POST | /mob/installs | Register an app install | Authenticated | |
| PUT | /mob/installs/:id/push-token | Update the Expo push token | Authenticated | |
| POST | /mob/sync | Upload a batch of offline actions (punches, visits, claim drafts) | Per action (att.punch.create, ffm.visit.create, clm.claim.create) | Per-item results; each item has its own idempotency key |
| GET / POST | /mob/kiosks | Kiosk devices for locations | mob.kiosk.enroll | |
| POST | /mob/kiosks/:id/enrolment-code | Issue a one-time QR enrolment code | mob.kiosk.enroll | |
| POST | /mob/kiosks/enroll | Tablet exchanges the QR code for a kiosk token | Enrolment code | |
| POST | /mob/kiosks/:id/revoke | Revoke a kiosk | mob.kiosk.enroll | |
| GET | /mob/kiosks/roster | Location roster for offline identification | mob.kiosk.operate | Names, codes, thumbnails, salted PIN hashes |
| POST | /mob/kiosks/punch | Kiosk punch (face or PIN) | mob.kiosk.operate + att.punch.create_on_behalf | Liveness and match scores stored |
| GET / POST | /mob/face-profiles | Face enrolment status; enrol | att.face.enroll | Consent required |
| DELETE | /mob/face-profiles/:id | Delete a face profile | att.face.enroll | Emits mob.face.deleted |
8. LVE: leave
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /lve/types | Leave types | lve.balance.read / lve.type.manage | |
| POST | /lve/types/from-template | Create a type from a template | lve.type.manage | |
| PATCH | /lve/types/:id | Edit or deactivate | lve.type.manage | |
| GET / POST | /lve/policies | Leave policies | lve.policy.manage | Same version and assignment sub-resources as ATT policies |
| POST | /lve/policies/:id/versions/:versionId/publish | Publish a version | lve.policy.manage | transitionMode for existing balances |
| POST | /lve/policies/:id/simulate | Simulate accruals for employees | lve.policy.manage | |
| GET | /lve/balances | Balances by employee and type, as of a date | lve.balance.read | Public |
| GET | /lve/ledger-entries | Ledger drill-down | lve.balance.read | |
| POST | /lve/ledger-entries | Manual adjustment with a reason | lve.balance.adjust | Emits lve.ledger.adjusted |
| POST | /lve/requests/quote | Day count, sandwich breakdown and balance impact without saving | lve.request.create | See §18.2 |
| GET / POST | /lve/requests | Leave requests; apply | lve.request.create (SELF) or lve.request.create_on_behalf | Public; lists are also visible with lve.request.approve in scope |
| GET | /lve/requests/:id | Request detail with workflow status | Requester or approver in scope | |
| PATCH | /lve/requests/:id | Edit while submitted | Requester | |
| POST | /lve/requests/:id/withdraw | Withdraw before a decision | Requester | |
| POST | /lve/requests/:id/cancel | Cancel approved future leave (starts a cancellation workflow) | lve.request.cancel | |
| GET | /lve/calendar | Team leave calendar | lve.balance.read | Scope-filtered |
| GET / POST | /lve/comp-off-credits | Comp-off credits; request a credit | lve.balance.read / lve.request.create | Approved through tasks (lve.compoff.approve) |
| GET | /lve/optional-holidays | Optional holidays available to me | org.holiday.read | |
| POST | /lve/optional-holidays/:holidayId/opt-in | Opt in (creates a request) | lve.request.create | |
| GET / POST | /lve/encashments | Encashment requests | lve.request.create | Approved through tasks (lve.encashment.approve) |
| GET | /lve/cycles | Leave cycles | lve.balance.read | |
| POST | /lve/cycles/:id/close | Year-end close (carry-forward, lapse, encashment) | lve.policy.manage | Job resource |
| POST | /lve/opening-balances/import | Import opening balances | lve.balance.adjust | dryRun |
| GET | /lve/payroll-bridge | LOP and encashment per employee for a pay period | pay.run.review | Consumed by PAY |
9. PAY: payroll
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /pay/components | Pay components | pay.component.manage | |
| PATCH | /pay/components/:id | Edit a component | pay.component.manage | |
| GET / POST | /pay/structures | Salary structure templates | pay.structure.manage | |
| GET / POST | /pay/structures/:id/versions | Structure versions and lines | pay.structure.manage | |
| POST | /pay/structures/:id/versions/:versionId/activate | Activate a version | pay.structure.manage | Immutable afterwards |
| POST | /pay/structures/preview | CTC breakdown preview for a structure and CTC | pay.compensation.update | Pure engine call |
| GET | /pay/compensations | Compensation history | pay.compensation.read | Public (explicit scope required); SELF for employees |
| POST | /pay/compensations | New compensation from a date (increment, promotion) | pay.compensation.update | Back-dated changes create arrears inputs; emits pay.compensation.changed |
| GET / PUT | /pay/employee-statutory/:employeeId | UAN, ESIC number, PF election, PT and LWF state, tax regime | pay.compensation.read / pay.compensation.update | Effective-dated |
| GET / POST | /pay/schedules | Pay schedules | pay.policy.manage | |
| GET | /pay/periods | Pay periods | pay.run.review | |
| POST | /pay/periods/generate | Generate periods for a financial year | pay.policy.manage | |
| GET / POST | /pay/policies | Payroll policies | pay.policy.manage | Same version and assignment sub-resources as ATT policies |
| GET / POST | /pay/statutory-registrations | EPF, ESI, PT, LWF and TAN registrations | org.company.read / org.company.manage | |
| GET | /pay/statutory-packs | Available statutory pack versions and change logs | pay.policy.manage | |
| POST | /pay/statutory-packs/:version/accept | Accept a pack version for this tenant | pay.statutory_pack.accept | |
| GET / POST | /pay/inputs | One-time inputs (arrears, variable pay, deductions) | pay.input.manage | Public |
| DELETE | /pay/inputs/:id | Remove a pending input | pay.input.manage | |
| POST | /pay/inputs/import | Import inputs | pay.input.manage | dryRun |
| GET / POST | /pay/runs | Pay runs; create a run | pay.run.review / pay.run.create | See §18.4 |
| GET | /pay/runs/:id | Run summary, totals, warnings | pay.run.review | |
| POST | /pay/runs/:id/prepare | Check the attendance lock, build the employee set, validate | pay.run.create | Job resource |
| POST | /pay/runs/:id/calculate | Calculate (all or selected employees) | pay.run.create | Job resource; body employeeIds optional |
| GET | /pay/runs/:id/employees | Per-employee results | pay.run.review | |
| GET | /pay/runs/:id/employees/:employeeId | Lines, input snapshot, explanation | pay.run.review | |
| POST | /pay/runs/:id/employees/:employeeId/hold | Hold or release one employee's salary | pay.run.create | |
| GET | /pay/runs/:id/variance | Variance against the previous period | pay.run.review | |
| POST | /pay/runs/:id/review | Mark reviewed and submit for approval | pay.run.review | Starts the approval workflow (pay.run.approve on the task) |
| POST | /pay/runs/:id/verify | Re-run the engine on stored snapshots and compare hashes | pay.run.review | Also runs automatically before lock |
| POST | /pay/runs/:id/lock | Lock: freeze snapshots, render payslips | pay.run.lock | MFA step-up; If-Match |
| POST | /pay/runs/:id/mark-paid | Record bank payment confirmation | pay.run.lock | Emits pay.run.paid |
| POST | /pay/runs/:id/reverse | Create a reversal run for a locked run | pay.run.reverse | |
| POST | /pay/runs/:id/cancel | Cancel an unlocked run | pay.run.create | |
| POST | /pay/bank-files | Generate a bank payout file for a run | pay.bank_file.generate | Public (GET of the file); job resource |
| POST | /pay/statutory-files | Generate ECR, ESIC, PT, LWF or 24Q files | pay.statutory_file.generate | Job resource |
| POST | /pay/journal-exports | Payroll journal for Tally, Zoho Books or CSV | pay.journal.export | Public |
| GET | /pay/payslips | Payslips | pay.payslip.read | SELF for employees |
| GET | /pay/payslips/:id/download | Payslip PDF (password-protected) | pay.payslip.read | Cache-Control: no-store |
| GET / POST | /pay/loans | Loans and salary advances | pay.loan.manage; employee self-request: pay.advance.request (SELF) | |
| GET / PUT | /pay/tax-declarations/:financialYear | My IT declaration (regime, items) | pay.tax_declaration.submit | SELF |
| POST | /pay/tax-declarations/:id/submit | Submit declarations | pay.tax_declaration.submit | Emits pay.declaration.submitted |
| POST | /pay/tax-proofs | Upload a proof | pay.tax_declaration.submit | |
| POST | /pay/tax-proofs/:id/verify | Accept or reduce a proof | pay.tax_declaration.verify | |
| GET | /pay/tax-projection | Projected annual tax under each regime | pay.payslip.read | employeeId=me |
| POST | /pay/form16/publish | Publish Form 16 for a financial year | pay.form16.publish | R2 |
10. NTF: notifications and workflow
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /ntf/tasks | My approval inbox (filter by request type, status, due) | ntf.inbox.read | Includes delegated tasks |
| GET | /ntf/tasks/:id | Task with subject summary and history | Assignee | |
| POST | /ntf/tasks/:id/approve | Approve | Assignee + the subject's approve key | See §18.3 |
| POST | /ntf/tasks/:id/reject | Reject (comment required) | Assignee + the subject's approve key | |
| POST | /ntf/tasks/:id/send-back | Return to the requester for changes | Assignee + the subject's approve key | Claims and timesheets |
| POST | /ntf/tasks/bulk-decide | Approve or reject up to 500 tasks | As above, per task | Per-item results |
| POST | /ntf/tasks/:id/reassign | Reassign a task | ntf.workflow.manage | Audited |
| GET | /ntf/instances/:id | Workflow instance with steps and actions | Requester, approver or ntf.workflow.manage | |
| GET / POST | /ntf/workflows | Workflow definitions per request type | ntf.workflow.manage | |
| POST | /ntf/workflows/:id/versions | Draft a new version (steps, conditions, SLA) | ntf.workflow.manage | |
| POST | /ntf/workflows/:id/versions/:versionId/publish | Publish from a date | ntf.workflow.manage | In-flight instances keep their version |
| GET / POST | /ntf/delegations | Delegate approvals | ntf.delegation.manage | SELF for employees; others for admins |
| DELETE | /ntf/delegations/:id | End a delegation | ntf.delegation.manage | |
| GET / PUT | /ntf/templates | Notification templates per event, channel and locale | ntf.template.manage | Preview endpoint: /ntf/templates/preview |
| GET / PUT | /ntf/channels | Channels, sender identity, reminder schedules | ntf.channel.manage | |
| GET | /ntf/notifications | My in-app notifications | ntf.inbox.read | |
| POST | /ntf/notifications/:id/read | Mark read | ntf.inbox.read | Also /ntf/notifications/read-all |
| GET / PUT | /ntf/preferences | My channel preferences | ntf.inbox.read | Security messages can't be disabled |
| GET / POST | /ntf/webhooks | Webhook subscriptions | iam.api_client.manage | Public; see Events & webhooks |
11. RPT: reporting
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /rpt/datasets | Datasets available to me | rpt.report.read | Salary datasets need rpt.payroll_report.read |
| POST | /rpt/datasets/:key/query | Run a dataset query (paged) | rpt.report.read | Scope-filtered; sensitive fields masked |
| GET / POST | /rpt/views | Saved views | rpt.report.read | Sharing needs the same scope for viewers |
| GET / POST | /rpt/exports | Export a dataset or view to XLSX, CSV or PDF | rpt.report.export | Public; job resource |
| GET | /rpt/exports/:id | Export status and download link | rpt.report.export | Public |
| GET / POST | /rpt/schedules | Scheduled email reports | rpt.report.schedule | |
| DELETE | /rpt/schedules/:id | Stop a schedule | rpt.report.schedule | |
| GET | /rpt/dashboards/:persona | Persona dashboard tiles | rpt.dashboard.read |
12. CLM: claims
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /clm/categories | Expense categories | clm.policy.manage | GET for all employees |
| GET / POST | /clm/policies | Expense policies (limits, per-diem, mileage) | clm.policy.manage | Same version and assignment sub-resources as ATT policies |
| POST | /clm/claims/check | Pre-submit policy and duplicate check | clm.claim.create | No side effects |
| GET / POST | /clm/claims | Claims; create a draft with lines | clm.claim.create | Public (GET); lists also visible to approvers in scope |
| GET | /clm/claims/:id | Claim with lines and workflow | Owner or approver in scope | |
| POST | /clm/claims/:id/lines | Add a line | clm.claim.create | Draft or sent back only |
| PATCH | /clm/claims/:id/lines/:lineId | Edit a line | clm.claim.create | |
| DELETE | /clm/claims/:id/lines/:lineId | Remove a line | clm.claim.create | |
| POST | /clm/claims/:id/submit | Submit for approval | clm.claim.create | See §18.5 |
| POST | /clm/claims/:id/withdraw | Withdraw before a decision | clm.claim.create | |
| PATCH | /clm/claims/:id/lines/:lineId/verification | Set the verified amount or reject one line | clm.claim.verify | Finance step |
| GET / POST | /clm/advances | Travel advances | clm.claim.create | Approved through tasks |
| GET | /clm/ledger-entries | Advance and payout ledger | clm.claim.create (SELF) or clm.claim.pay | |
| GET / POST | /clm/payout-batches | Payout batches (payroll or bank file) | clm.claim.pay | |
| POST | /clm/payout-batches/:id/confirm | Confirm a batch as paid | clm.claim.pay | Emits clm.claim.paid |
Claim decisions go through /ntf/tasks: the manager step needs clm.claim.approve, and the finance step needs clm.claim.verify.
13. EXT: exit and full-and-final
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /ext/separations | Separations; submit a resignation | ext.resignation.submit (SELF) | Lists visible to approvers in scope |
| GET | /ext/separations/:id | Separation detail | Employee, approver or HR in scope | |
| POST | /ext/separations/:id/withdraw | Withdraw a resignation | ext.resignation.submit | Before approval, or per policy |
| POST | /ext/separations/:id/lwd-proposals | Propose a different last working day | ext.resignation.approve or the employee | Negotiation history |
| POST | /ext/terminations | Initiate a termination | ext.termination.initiate | Optional immediate access revocation |
| GET | /ext/separations/:id/clearance | Clearance items | ext.clearance.update | Items are emp_task rows |
| POST | /ext/separations/:id/clearance/:taskId/complete | Clear an item, with recoveries | ext.clearance.update | |
| GET / PUT | /ext/separations/:id/exit-interview | Exit interview | The employee (submit); HR (read) | Restricted visibility |
| POST | /ext/fnf-settlements | Compute the F&F settlement (creates an FNF pay run) | ext.settlement.compute | Job resource |
| GET | /ext/fnf-settlements/:id | Settlement statement | ext.settlement.compute or ext.settlement.approve | |
| POST | /ext/fnf-settlements/:id/approve | Approve the settlement | ext.settlement.approve | |
| POST | /ext/separations/:id/letters | Relieving and experience letters | emp.letter.generate | |
| GET / POST | /ext/reasons | Separation and termination reasons | ext.master.manage |
Resignation approval goes through /ntf/tasks with ext.resignation.approve.
14. ENG: engagement
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /eng/feed | My feed: announcements, recognitions, celebrations | eng.announcement.read | |
| GET / POST | /eng/announcements | Announcements; create a draft | eng.announcement.read / eng.announcement.publish | |
| PATCH | /eng/announcements/:id | Edit a draft or scheduled announcement | eng.announcement.publish | |
| POST | /eng/announcements/:id/publish | Publish or schedule | eng.announcement.publish | Audience expansion job |
| POST | /eng/announcements/:id/stop | Stop a published announcement | eng.announcement.publish | |
| POST | /eng/announcements/:id/acknowledge | Acknowledge | eng.announcement.read | |
| GET | /eng/announcements/:id/reads | Read and acknowledgement report | eng.announcement.publish | |
| GET / POST | /eng/recognition-types | Recognition types and badges | eng.program.manage | |
| GET / POST | /eng/recognitions | Recognitions; send one | eng.recognition.give | |
| POST | /eng/recognitions/:id/reactions | React | eng.recognition.give |
15. FFM: field force and CRM
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /ffm/pipelines | Pipelines and stages | ffm.pipeline.manage | Templates available |
| GET / POST | /ffm/opportunities | Opportunities (leads) | ffm.account.read / ffm.account.manage | Public |
| PATCH | /ffm/opportunities/:id | Edit | ffm.account.manage | |
| POST | /ffm/opportunities/:id/stage | Move to a stage | ffm.account.manage | Emits ffm.opportunity.stage_changed |
| POST | /ffm/opportunities/:id/reassign | Reassign owner | ffm.lead.assign | |
| GET / POST | /ffm/associates | Client associates | ffm.account.read / ffm.account.manage | |
| GET / POST | /ffm/activities | Calls, visits, follow-ups | ffm.account.read / ffm.account.manage | |
| GET / POST | /ffm/visits | Visits; check in at a client | ffm.visit.read / ffm.visit.create | Public (GET); location verified against the account |
| POST | /ffm/visits/:id/check-out | Check out | ffm.visit.create | |
| GET / POST | /ffm/beat-plans | Beat and route plans | ffm.visit.read / ffm.beat.manage | |
| POST | /ffm/sessions | Start a tracking session (after punch-in, with consent) | ffm.visit.create | |
| POST | /ffm/sessions/:id/end | End tracking | ffm.visit.create | Also automatic at punch-out |
| POST | ingest:/ffm/sessions/:id/points | Upload location points (batched) | ffm.visit.create | On the ingest host; C4 data |
| GET | /ffm/tracking/live | Live team positions and battery | ffm.tracking.read | TEAM scope; audited |
| GET | /ffm/sessions/:id/trail | A past trail | ffm.tracking.read | Within the retention window |
| GET | /ffm/daily-travel | Daily travel summaries | ffm.travel.submit (SELF) or ffm.travel.approve | |
| POST | /ffm/daily-travel/:id/submit | Submit a day's kms | ffm.travel.submit | |
| POST | /ffm/daily-travel/:id/revise | Revise kms with a reason | ffm.travel.approve | Approval through tasks; emits ffm.travel.verified |
| GET / POST | /ffm/payments | Field payment collections | ffm.account.read / ffm.payment.collect | |
| GET / POST | /ffm/targets | Sales targets | ffm.target.manage |
16. TSM: timesheets
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET / POST | /tsm/projects | Projects | tsm.project.manage | GET also for members |
| PATCH | /tsm/projects/:id | Edit a project | tsm.project.manage | |
| GET / PUT | /tsm/projects/:id/members | Members, roles, allocation | tsm.project.manage | |
| GET / POST | /tsm/tasks | Tasks | tsm.project.manage | |
| GET | /tsm/timesheets | Timesheets | tsm.timesheet.submit (SELF) or tsm.timesheet.approve | |
| PUT | /tsm/timesheets/:weekStart | Save my weekly grid | tsm.timesheet.submit | |
| POST | /tsm/timesheets/:id/submit | Submit for approval | tsm.timesheet.submit | Approved through tasks (tsm.timesheet.approve) |
| GET | /tsm/defaulters | Missing timesheets | tsm.timesheet.approve | |
| POST | /tsm/defaulters/notify | Nudge defaulters | tsm.timesheet.approve | |
| GET / POST | /tsm/period-locks | Lock a timesheet period | tsm.project.manage |
17. LMS: learning
| Method | Path | Purpose | Permission | Notes |
|---|---|---|---|---|
| GET | /lms/catalogue | Courses open or assigned to me | lms.course.read | |
| GET / POST | /lms/courses | Courses (author view) | lms.course.manage | |
| PATCH | /lms/courses/:id | Edit modules, lessons and assessments | lms.course.manage | SCORM upload via /sys/files |
| POST | /lms/courses/:id/publish | Publish | lms.course.manage | |
| GET / POST | /lms/assignment-rules | Automatic assignment rules | lms.course.assign | |
| GET / POST | /lms/enrollments | Enrolments; assign learners | lms.course.read / lms.course.assign | Public (GET) |
| POST | /lms/enrollments/:id/progress | Record lesson progress | lms.course.read | |
| POST | /lms/assessments/:id/attempts | Submit an attempt | lms.course.read | Answer keys never leave the server |
| POST | /lms/attempts/:id/grade | Grade a free-text attempt | lms.assessment.grade | |
| GET | /lms/certificates/:id/download | Certificate PDF | lms.course.read |
18. Worked examples
All examples use the placeholder tenant acme.spyrl.app. IDs are shortened for readability.
18.1 Punch (mobile, selfie + geo-fence)
The selfie is uploaded first (POST /sys/files with purpose SELFIE, then the presigned PUT, then complete).
POST /api/v1/att/punches HTTP/1.1
Host: acme.spyrl.app
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
Idempotency-Key: 0193b3a1-6c55-7d10-9a2b-3c4d5e6f7081
Spyrl-Device-Id: 01934f00-aaaa-7bbb-8ccc-dddddddddddd
Spyrl-App-Version: 1.4.2
Content-Type: application/json
{
"clientRef": "0193b3a1-6c55-7d10-9a2b-3c4d5e6f7081",
"direction": "IN",
"source": "MOBILE",
"deviceTime": "2026-11-04T09:02:11+05:30",
"elapsedSinceSyncMs": 0,
"location": { "latitude": 17.493812, "longitude": 78.391204, "accuracyM": 12.5 },
"network": { "wifiBssid": "a4:2b:b0:11:22:33" },
"selfieFileId": "0193b3a1-5f00-7e00-8a00-112233445566"
}
201 Created:
{
"id": "0193b3a1-7001-7c2d-9e3f-405162738495",
"employeeId": "01929a44-1111-7222-8333-444455556666",
"punchedAt": "2026-11-04T09:02:12+05:30",
"direction": "IN",
"source": "MOBILE",
"status": "ACCEPTED",
"geofence": { "result": "INSIDE", "geofenceId": "01929b20-...", "distanceM": 38 },
"faceMatch": { "result": "NOT_CHECKED" },
"flags": [],
"dayPreview": {
"workDate": "2026-11-04",
"shift": { "code": "GEN", "start": "09:00", "end": "18:00" },
"provisionalStatus": "P",
"lateMinutes": 2,
"withinGrace": true
}
}
Outside the fence with the policy in ENFORCE mode, the API returns 422:
{
"type": "https://docs.spyrl.app/problems/attendance-outside-geofence",
"title": "You are outside the allowed location",
"status": 422,
"detail": "Nearest site 'Kukatpally Office' is 1.4 km away (allowed 200 m).",
"code": "att.outside_geofence",
"fallbacks": ["QR", "REGULARIZATION"]
}
The punch emits att.punch.recorded, which schedules att.day.compute for the employee and date.
18.2 Leave apply (quote, then create)
POST /api/v1/lve/requests/quote
{ "employeeId": "me", "leaveTypeId": "01929c01-...-cl", "fromDate": "2026-11-13", "toDate": "2026-11-16", "fromHalf": "FULL", "toHalf": "FULL" }
200 OK:
{
"requestedDays": "2.00",
"chargedDays": "4.00",
"days": [
{ "date": "2026-11-13", "dayKind": "WORKING", "charge": "1.00", "sandwiched": false },
{ "date": "2026-11-14", "dayKind": "WEEK_OFF", "charge": "1.00", "sandwiched": true },
{ "date": "2026-11-15", "dayKind": "WEEK_OFF", "charge": "1.00", "sandwiched": true },
{ "date": "2026-11-16", "dayKind": "WORKING", "charge": "1.00", "sandwiched": false }
],
"balance": { "before": "6.50", "reserved": "0.00", "after": "2.50" },
"policy": { "policyVersionId": "01929d10-...", "rule": "Sandwich across week-offs applies to Casual Leave" },
"warnings": [ { "code": "lve.team_overlap", "message": "2 of 6 team members are on leave on 2026-11-13" } ]
}
POST /api/v1/lve/requests
Idempotency-Key: 0193b3b0-0101-7202-8303-040506070809
{
"employeeId": "me",
"leaveTypeId": "01929c01-...-cl",
"fromDate": "2026-11-13",
"toDate": "2026-11-16",
"fromHalf": "FULL",
"toHalf": "FULL",
"reason": "Family function",
"contactDuringLeave": "+919876543210"
}
201 Created:
{
"id": "0193b3b0-2222-7333-8444-555566667777",
"status": "IN_APPROVAL",
"requestedDays": "2.00",
"chargedDays": "4.00",
"balanceReserved": "4.00",
"workflow": {
"instanceId": "0193b3b0-8888-7999-8aaa-bbbbccccdddd",
"currentStep": { "stepNo": 1, "assignees": [ { "userId": "01928f10-...", "name": "Priya Rao" } ], "dueAt": "2026-11-06T09:30:00+05:30" }
}
}
Events: lve.request.submitted, then ntf.instance.started and ntf.task.created (the approver gets a push and an email).
18.3 Leave approve (through the task inbox)
GET /api/v1/ntf/tasks?status=OPEN&requestType=lve.request&limit=20
{
"data": [
{
"id": "0193b3b1-1234-7abc-8def-0123456789ab",
"requestType": "lve.request",
"subject": { "id": "0193b3b0-2222-7333-8444-555566667777", "summary": "Ravi Kumar · Casual Leave · 13–16 Nov (4.00 days incl. sandwich)" },
"stepNo": 1,
"dueAt": "2026-11-06T09:30:00+05:30",
"context": { "balanceAfter": "2.50", "teamOverlap": 2 }
}
],
"page": { "limit": 20, "nextCursor": null, "hasMore": false }
}
POST /api/v1/ntf/tasks/0193b3b1-1234-7abc-8def-0123456789ab/approve
Idempotency-Key: 0193b3b2-0000-7000-8000-000000000001
{ "comment": "Approved. Please hand over the GST filing to Anil." }
200 OK:
{
"task": { "id": "0193b3b1-1234-7abc-8def-0123456789ab", "status": "APPROVED", "actedAt": "2026-11-04T11:15:40+05:30" },
"instance": { "id": "0193b3b0-8888-7999-8aaa-bbbbccccdddd", "status": "APPROVED" },
"subject": { "type": "lve.request", "id": "0193b3b0-2222-7333-8444-555566667777", "status": "APPROVED" }
}
In one transaction: the task and instance close, the leave request moves to APPROVED, and an AVAIL ledger entry of −4.00 days is posted. Events: ntf.instance.completed and lve.request.approved (ATT updates att_leave_mark and recomputes 13–16 Nov; NTF informs the employee).
18.4 Payroll run: create, calculate, lock
POST /api/v1/pay/runs
Idempotency-Key: 0193c000-0000-7000-8000-00000000a001
{ "periodId": "0193a000-...-2026-11", "runType": "REGULAR" }
201 Created returns the run in status DRAFT (the full shape is shown below after calculation).
POST /api/v1/pay/runs/0193c001-.../prepare
POST /api/v1/pay/runs/0193c001-.../calculate
Idempotency-Key: 0193c000-0000-7000-8000-00000000a003
202 Accepted with Location: /api/v1/sys/jobs/0193c002-.... When the job succeeds:
GET /api/v1/pay/runs/0193c001-...
{
"id": "0193c001-...",
"periodId": "0193a000-...-2026-11",
"runType": "REGULAR",
"status": "CALCULATED",
"calcVersion": 1,
"engineVersion": "1.6.0",
"statPackVersion": "2026.10.1",
"employeeCount": 214,
"totals": {
"grossPaise": 712455000,
"deductionsPaise": 98231400,
"netPaise": 614223600,
"employerCostPaise": 61877300
},
"checks": {
"attendancePeriodLocked": true,
"errors": 0,
"warnings": [
{ "code": "pay.variance_high", "count": 3, "message": "Net pay changed by more than 20% from October" },
{ "code": "pay.missing_bank_account", "count": 1, "message": "1 employee has no verified bank account" }
]
},
"etag": "\"run-7\""
}
The reviewer checks /pay/runs/:id/variance, then calls POST /pay/runs/:id/review, which opens an approval task (pay.run.approve, with segregation of duties). After approval:
POST /api/v1/pay/runs/0193c001-.../lock
Idempotency-Key: 0193c000-0000-7000-8000-00000000a009
If-Match: "run-9"
{ "publishPayslips": true, "payslipPublishAt": "2026-11-30T18:00:00+05:30" }
200 OK:
{
"id": "0193c001-...",
"status": "LOCKED",
"lockedAt": "2026-11-28T16:42:03+05:30",
"verification": { "employeesVerified": 214, "hashMismatches": 0 },
"payslips": { "status": "RENDERING", "jobId": "0193c003-..." },
"next": ["POST /api/v1/pay/bank-files", "POST /api/v1/pay/statutory-files"]
}
Locking requires MFA step-up. It emits pay.run.locked (ATT closes the period; CLM and EXT mark settled items) and, when payslips are published, pay.payslip.published.
18.5 Claim submit
POST /api/v1/clm/claims
Idempotency-Key: 0193d000-0000-7000-8000-00000000b001
{
"title": "Client visit - Warangal",
"payoutMode": "PAYROLL",
"lines": [
{
"categoryId": "0192e001-...-food",
"expenseDate": "2026-11-02",
"billNo": "INV-88213",
"vendorName": "Hotel Kakatiya",
"vendorGstin": "36ABCDE1234F1Z5",
"amountPaise": 64000,
"gstPaise": 3048,
"attachmentFileIds": ["0193cfff-1111-7222-8333-444455556666"]
},
{
"categoryId": "0192e002-...-mileage",
"expenseDate": "2026-11-02",
"quantity": "142.5",
"sourceRef": "0193cf00-...-daily-travel"
}
]
}
201 Created (draft, with policy results):
{
"id": "0193d001-...",
"claimNo": "CL-2026-00418",
"status": "DRAFT",
"requestedPaise": 491500,
"lines": [
{ "id": "0193d002-...", "amountPaise": 64000, "policyResult": { "status": "OK" } },
{ "id": "0193d003-...", "amountPaise": 427500, "policyResult": { "status": "OK", "rate": "30.00 per km", "basis": "142.5 km verified travel" } }
],
"warnings": []
}
POST /api/v1/clm/claims/0193d001-.../submit
Idempotency-Key: 0193d000-0000-7000-8000-00000000b002
200 OK:
{
"id": "0193d001-...",
"status": "SUBMITTED",
"submittedAt": "2026-11-04T19:20:00+05:30",
"workflow": { "instanceId": "0193d004-...", "currentStep": { "stepNo": 1, "name": "Manager approval" } }
}
Events: clm.claim.submitted; clm.duplicate.suspected if a line matches an earlier bill. When the last step approves, clm.claim.approved creates a pay_input of type REIMBURSEMENT for the next run.
18.6 Tenant provisioning (control plane)
POST https://cp.spyrl.app/api/v1/tenants
Authorization: Bearer <platform token>
Idempotency-Key: 0193e000-0000-7000-8000-00000000c001
{
"slug": "acme",
"companyCode": "ACME01",
"legalName": "Acme Industries Private Limited",
"planCode": "STANDARD",
"addons": [],
"region": "ap-south-1",
"ownerEmail": "owner@acme.example",
"ownerName": "Sunita Reddy",
"trialDays": 30,
"sampleData": false
}
202 Accepted with Location: /api/v1/tenants/0193e001-.../provisioning-runs/0193e002-...:
{
"tenantId": "0193e001-...",
"status": "PROVISIONING",
"cellId": "0192f000-...-p1",
"provisioningRunId": "0193e002-...",
"host": "acme.spyrl.app"
}
GET https://cp.spyrl.app/api/v1/tenants/0193e001-.../provisioning-runs/0193e002-...
{
"id": "0193e002-...",
"status": "SUCCEEDED",
"steps": [
{ "key": "VALIDATE", "status": "SUCCEEDED" },
{ "key": "SELECT_CELL", "status": "SUCCEEDED", "detail": "p1" },
{ "key": "CREATE_TENANT_ROW", "status": "SUCCEEDED" },
{ "key": "SEED_REFERENCE_DATA", "status": "SUCCEEDED", "detail": "statutory pack 2026.10.1" },
{ "key": "SEED_ROLES", "status": "SUCCEEDED" },
{ "key": "SEED_POLICIES", "status": "SUCCEEDED" },
{ "key": "CREATE_OWNER_INVITE", "status": "SUCCEEDED" },
{ "key": "CONFIGURE_DOMAIN", "status": "SKIPPED", "detail": "wildcard subdomain" },
{ "key": "SEND_WELCOME", "status": "SUCCEEDED" }
],
"startedAt": "2026-11-04T10:00:00Z",
"finishedAt": "2026-11-04T10:01:12Z"
}
A duplicate slug or company code returns 409 conflict. A failed step leaves the run FAILED; POST …/retry resumes from that step. Success emits plt.tenant.provisioned.