Skip to main content

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 at https://api.spyrl.app/v1 with the same path. Control-plane paths are relative to https://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 example lve.request.approve for a leave task.
  • GET / POST on one row means "list" and "create" on the same collection. PATCH updates use If-Match. Mutating POSTs take an Idempotency-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)​

MethodPathPurposePermissionNotes
GET/plt/subscriptionPlan, add-ons, usage this month, invoicesplt.subscription.read
POST/plt/subscription/changesChange plan or add-onsplt.subscription.manageProxied to the control plane
GET/plt/entitlementsEffective features and limits for this tenantAuthenticatedDrives navigation in web and mobile
GET/plt/support-accessSupport access requests and the access logplt.support_access.approveVisible to the tenant by design
POST/plt/support-access/:id/approveApprove a support sessionplt.support_access.approveMFA step-up
POST/plt/support-access/:id/denyDeny a requestplt.support_access.approve
POST/plt/support-access/:id/revokeEnd an active support sessionplt.support_access.approve
POST/plt/data-exportsRequest a full tenant export (DPDP portability, offboarding)plt.data_export.requestJob resource
POST/plt/closure-requestsRequest account closure and deletionplt.tenant.close_requestCooling-off period applies

1.2 Public (unauthenticated)​

MethodPathPurposePermissionNotes
GET/public/tenants/resolve?code=Company code → tenant host, name and branding (mobile first run)NoneRate-limited per IP; reveals nothing for unknown codes beyond 404
GET/public/lms/certificates/:no/verifyVerify a certificate numberNoneReturns name, course and date only
GET/org/brandingLogo and colours for the login pageNoneOn the tenant host; cacheable

1.3 Control plane (https://cp.spyrl.app/api/v1)​

MethodPathPurposePermissionNotes
POST/signupsSelf-serve trial sign-upNone (captcha, email verification)Starts provisioning
GET / POST/tenantsSearch tenants; create and provision a tenantplt.tenant.read / plt.tenant.managePOST returns 202 with a provisioning run (see §18.6)
GET/tenants/:idTenant detail: plan, cell, status, healthplt.tenant.read
PATCH/tenants/:idEdit display name, trial end, notesplt.tenant.manage
GET/tenants/:id/provisioning-runs/:runIdProvisioning progress by stepplt.tenant.read
POST/tenants/:id/provisioning-runs/:runId/retryResume a failed run from the failed stepplt.tenant.manageSteps are idempotent
POST/tenants/:id/suspendSuspend (read-only)plt.tenant.manageNever during the payroll window without a second approver
POST/tenants/:id/reactivateReactivateplt.tenant.manage
POST/tenants/:id/movesMove a tenant to another cellplt.tenant.manageSecond approver; job resource
POST/tenants/:id/exportsExport a tenant's dataplt.tenant.manageJob resource
POST/tenants/:id/deletionSchedule deletion after the export windowplt.tenant.manageSecond approver; blocked by legal hold
GET / POST/tenants/:id/domainsList or add a custom domainplt.tenant.manageEnterprise
POST/tenants/:id/domains/:domainId/verifyCheck the TXT record and issue the certificateplt.tenant.manage
PUT/tenants/:id/subscriptionSet plan and add-onsplt.plan.manage
PUT/tenants/:id/entitlement-overridesTime-boxed limit overridesplt.plan.manageReason required
GET/tenants/:id/usageDaily usage metricsplt.tenant.read
GET / PUT/plans, /plans/:idPlans and entitlementsplt.plan.manage
GET / POST/invoicesInvoices; create a manual invoice or credit noteplt.invoice.manage
GET / PUT/flags, /flags/:keyFlags and targeting rulesplt.flag.manage
POST/flags/:key/killToggle a documented kill switchplt.flag.kill_switchIncident use
GET / POST/cellsCell registry; create a cellplt.cell.manage
PATCH/cells/:idResize, maintenance window, statusplt.cell.manage
POST/releases/:id/rolloutsStart a wave rolloutplt.release.rollout
POST/rollouts/:id/pause / resume / abortControl a rolloutplt.release.rollout
PUT/mobile/min-versionMinimum supported app version per platformplt.release.rolloutDrives 426
POST/support-sessionsRequest access to a tenantplt.support.requestReason and ticket required
POST/support-sessions/:id/emergency-approveEmergency access without tenant consentplt.support.emergency_approveSecond person
POST/statutory-packsDraft a statutory pack versionplt.statutory_pack.publish
POST/statutory-packs/:id/publishPublish to all cellsplt.statutory_pack.publishEmits plt.statutory_pack.published
POST/announcementsPlatform banner for a plan, cell or tenantplt.announcement.publish
GET/audit-eventsPlatform audit logplt.audit.read
GET / POST/platform-usersManage Spyrl staff accountsplt.platform_user.manage
POST/billing/razorpay/webhookRazorpay subscription eventsRazorpay signatureIdempotent on the event ID

2. Kernel (sys)​

MethodPathPurposePermissionNotes
POST/sys/filesRegister a file and get a presigned upload URLAuthenticated (the purpose decides who may attach it)Public
POST/sys/files/:id/completeConfirm the upload and start scanningUploaderPublic
GET/sys/files/:id/downloadRedirect to a short-lived download URLRead permission on the owning entityPublic; audited for C3/C4 files
GET/sys/jobs/:idStatus of an async jobRequester, or the job's admin permissionPublic
POST/sys/jobs/:id/cancelCancel a jobRequester
GET/sys/policy-kindsPolicy kinds and schema versionsAny *.policy.manage
GET/sys/policy-kinds/:kind/schemaJSON Schema for a policy kind (drives the policy editor)Any *.policy.manage
GET/openapi.jsonThe internal OpenAPI 3.1 documentAuthenticatedThe public spec is at https://api.spyrl.app/v1/openapi.json

3. ORG: organisation and masters​

MethodPathPurposePermissionNotes
GET/org/companyCompany profileorg.company.readPublic
PATCH/org/companyEdit the company profileorg.company.manage
PATCH/org/brandingLogo, colours, sender nameorg.branding.manage
GET/org/setupSetup checklist and go-live readiness scoreorg.company.manage
GET / POST/org/legal-entitiesLegal entitiesorg.company.read / org.company.manageMulti-entity tenants: decision pending
PATCH/org/legal-entities/:idEdit a legal entityorg.company.manage
GET / POST/org/locationsLocations (state, time zone, ID sequence)org.company.read / org.location.managePublic (GET)
PATCH/org/locations/:idEdit a locationorg.location.manageEmits org.location.updated
GET / POST/org/locations/:id/geofencesGeo-fences and allowed networksatt.geofence.manage
PATCH/org/locations/:id/geofences/:fenceIdEdit a fenceatt.geofence.manageEmits org.geofence.changed
PUT/org/locations/:id/calendarAssign a holiday calendar from a dateorg.holiday.manageEffective-dated
GET / POST/org/departmentsDepartmentsorg.company.read / org.department.managePublic (GET)
PATCH/org/departments/:idEdit a departmentorg.department.manage
POST/org/departments/:id/deactivateDeactivate (blocked while employees are assigned)org.department.manage
PUT/org/departments/:id/designationsAllowed designations for a departmentorg.designation.manage
GET / POST/org/cost-centersCost centresorg.department.manage
GET / POST/org/designationsDesignationsorg.company.read / org.designation.managePublic (GET)
PATCH/org/designations/:idEdit or deactivateorg.designation.manage
GET / POST/org/gradesGradesorg.designation.manage
GET / POST/org/holiday-calendarsHoliday calendarsorg.holiday.read / org.holiday.managePublic (GET)
POST/org/holiday-calendars/:id/holidaysAdd holidays (bulk)org.holiday.manageEmits org.holiday.changed
DELETE/org/holiday-calendars/:id/holidays/:holidayIdRemove a holidayorg.holiday.manageTriggers recompute
GET / POST/org/custom-fieldsCustom field definitionsorg.custom_field.manage
GET / PATCH/org/id-sequencesEmployee and document number sequencesorg.company.manage
GET / POST/org/bank-accountsEmployer debit accounts for bank filesorg.company.manageAccount numbers masked
GET/org/chartOrg chartorg.chart.read
GET / POST/org/accountsShared client accounts (FFM and TSM)ffm.account.read / ffm.account.managePublic
PATCH/org/accounts/:idEdit an accountffm.account.manage
GET / POST/org/contactsAccount contactsffm.account.read / ffm.account.manage

4. IAM: identity and access​

MethodPathPurposePermissionNotes
POST/iam/auth/loginPassword loginNoneReturns tokens or an MFA challenge
POST/iam/auth/mfa/verifyComplete an MFA challenge or step-upChallenge token
POST/iam/auth/refreshRotate the refresh tokenRefresh cookie or tokenReuse detection
POST/iam/auth/logoutEnd the sessionAuthenticated
GET/iam/auth/sso/:connectionId/startStart OIDC or SAML loginNone
GET / POST/iam/auth/sso/callbackSSO callbackIdP responseJIT link to existing employees only
POST/iam/auth/password/forgotSend a reset linkNoneSame response whether or not the user exists
POST/iam/auth/password/resetSet a new password with a reset tokenReset token
POST/iam/invites/:token/acceptActivate an account from an inviteInvite tokenSets password; MFA enrolment if required
GET/iam/meCurrent user, employee and tenant summaryAuthenticated
GET/iam/me/permissionsEffective permissions with scopesAuthenticatedFor UI gating
POST/iam/me/passwordChange passwordAuthenticated
POST/iam/me/mfa/totpStart TOTP enrolmentAuthenticatedReturns the otpauth URI
POST/iam/me/mfa/totp/verifyConfirm enrolment; returns recovery codesAuthenticated
GET/iam/sessionsList sessionsiam.session.revokeSELF for employees
DELETE/iam/sessions/:idRevoke a sessioniam.session.revoke
GET/iam/usersUser accounts and statusiam.user.read
POST/iam/users/:id/inviteSend or resend the activation inviteiam.user.inviteBulk variant: /iam/users/invites
POST/iam/users/:id/deactivateDeactivateiam.user.deactivateRevokes sessions and devices
POST/iam/users/:id/reactivateReactivateiam.user.deactivate
POST/iam/users/:id/unlockClear a lockoutiam.user.deactivate
GET/iam/permissionsPermission registryiam.role.read
GET / POST/iam/rolesRolesiam.role.read / iam.role.manage
POST/iam/roles/:id/cloneClone a system roleiam.role.manage
PATCH/iam/roles/:idRename or describe a custom roleiam.role.manage
PUT/iam/roles/:id/permissionsSet grants (permission + max scope)iam.role.manage
GET / POST/iam/role-assignmentsAssign roles with scope and filtersiam.role.read / iam.role.assignOnly roles whose grants the caller holds
DELETE/iam/role-assignments/:idRemove an assignmentiam.role.assign
POST/iam/devicesRegister the current mobile deviceAuthenticatedMay become PENDING_APPROVAL
GET/iam/devicesDevices awaiting approval or activeiam.device.approve
POST/iam/devices/:id/approveApprove a deviceiam.device.approve
POST/iam/devices/:id/revokeRevoke a device (remote sign-out)iam.device.approve
GET / PUT/iam/policies/:kindPassword, session, MFA and device policiesiam.policy.manageTyped policy documents
GET / POST/iam/sso-connectionsOIDC and SAML connectionsiam.sso.manage
PATCH/iam/sso-connections/:idEdit or enforce SSOiam.sso.manage
GET / POST/iam/api-clientsPublic API clientsiam.api_client.manageSecret shown once
POST/iam/api-clients/:id/rotate-secretRotate a client secretiam.api_client.manage
DELETE/iam/api-clients/:idRevoke a clientiam.api_client.manage
GET/iam/audit-eventsSearch the audit logiam.audit.read
POST/iam/audit-exportsExport the audit logiam.audit.exportJob resource; MFA step-up

5. EMP: core HR​

MethodPathPurposePermissionNotes
GET/emp/directoryDirectory (name, photo, designation, work contact)emp.directory.readSearch with q
GET / POST/emp/employeesList employees; add a new hireemp.profile.read / emp.profile.createPublic
GET/emp/employees/:idEmployee profile (non-sensitive)emp.profile.readPublic
PATCH/emp/employees/:idEdit the record directlyemp.profile.updatePublic
GET/emp/employees/:id/personalPersonal data, identity numbers maskedemp.profile.read
PATCH/emp/employees/:id/personalEdit personal and identity dataemp.sensitive.updateEncrypted fields
POST/emp/employees/:id/personal/revealUnmask Aadhaar or PAN for one viewemp.sensitive.readAudited; MFA step-up
GET / POST/emp/employees/:id/job-assignmentsJob history; record a transfer, promotion or manager change from a dateemp.profile.read / emp.job.changePublic; emits emp.job.changed
GET / POST/emp/employees/:id/bank-accountsBank accountsemp.sensitive.read / emp.sensitive.updateMasked by default
GET / POST/emp/employees/:id/family-membersFamily and nomineesemp.profile.read / emp.profile.updateEmployees use change requests
GET / POST/emp/employees/:id/educationEducationemp.profile.read / emp.profile.update
GET / POST/emp/employees/:id/experienceWork experienceemp.profile.read / emp.profile.update
POST/emp/employees/:id/confirm-probationConfirm or extend probationemp.probation.confirmEmits emp.employee.confirmed
POST/emp/employees/:id/preboarding-linkSend the self-onboarding linkemp.onboarding.manageSingle-use, expiring
GET / POST/emp/preboarding/:tokenLoad and submit the self-onboarding formPre-boarding tokenEmits emp.preboarding.submitted
GET / POST/emp/documentsDocuments; uploademp.document.read / emp.document.uploadFiles via /sys/files
POST/emp/documents/:id/verifyVerify a documentemp.document.verify
POST/emp/documents/:id/rejectReject with a reasonemp.document.verify
GET / POST/emp/checklist-templatesOnboarding and offboarding checklistsemp.onboarding.manage
GET/emp/tasksChecklist tasks (mine, my team, all)emp.onboarding.manage, or task owner
POST/emp/tasks/:id/completeComplete a task with evidenceTask owner or emp.onboarding.manageExit clearance uses ext.clearance.update
GET / POST/emp/change-requestsProfile change requestsemp.change_request.createApproved through /ntf/tasks (emp.change_request.approve)
GET / POST/emp/induction-programsInduction programmes and attendeesemp.onboarding.manage
GET / PUT/emp/letter-templatesLetter templatesntf.template.manage
POST/emp/lettersGenerate a letter (appointment, confirmation, relieving)emp.letter.generateJob resource for bulk
POST/emp/importsUpload an import file and run a dry runemp.import.rundryRun=true by default
GET/emp/imports/:idValidation reportemp.import.run
POST/emp/imports/:id/commitCommit a validated importemp.import.runIdempotent per row
GET / POST/emp/consentsMy consents (face, location, notices)AuthenticatedSELF only
POST/emp/consents/:id/withdrawWithdraw a consentAuthenticatedTriggers template deletion

6. ATT: attendance​

MethodPathPurposePermissionNotes
POST/att/punchesPunch in or out (web or mobile, selfie and location)att.punch.createIdempotency-Key = clientRef; see §18.1
GET/att/punchesPunch log by employee and date rangeatt.record.readPublic; time-range filter required
POST/att/punches/importImport punches (CSV, Excel, device logs)att.import.runPublic; dryRun; job resource
GET/att/daysComputed attendance daysatt.record.readPublic
PATCH/att/days/:idDirect correction with a reasonatt.record.editAudited; blocked in locked periods
POST/att/days/recomputeRecompute a range of employee-daysatt.day.recomputeJob resource; refused for locked periods
GET/att/calendarMonth calendar with legend, holidays and leaveatt.record.reademployeeId=me
GET/att/rosterRoster view by location and datesatt.record.read
GET / POST/att/requestsAttendance requests: work type (WFH, OD, remote), regularization, permission, shift change, overtimeBy type: att.work_request.request, att.regularization.request, att.permission.request, att.shift_change.requestOn-behalf entry needs the matching approve key (att.work_request.approve for work types)
PATCH/att/requests/:idEdit while submittedRequester
POST/att/requests/:id/withdrawWithdrawRequester
POST/att/requests/:id/cancelAsk to cancel an approved future requestRequesterStarts a cancellation workflow
GET / POST/att/shiftsShift mastersatt.shift.manage
PATCH/att/shifts/:idEdit a shiftatt.shift.manageTriggers recompute for open periods
GET / POST/att/rotationsRotation sequencesatt.roster.manage
GET / POST/att/weekoff-patternsWeek-off patternsatt.roster.manage
GET / POST/att/shift-assignmentsAssign shifts, rotations and week-offs for a date range (bulk)att.roster.manageUp to 500 employees per call
GET / POST/att/policiesAttendance and overtime policiesatt.policy.manage
GET / POST/att/policies/:id/versionsVersions; create a draftatt.policy.manageValidated against the kind's schema
POST/att/policies/:id/versions/:versionId/publishPublish from a dateatt.policy.manageConflict check; back-dating into locked periods is blocked
GET / POST/att/policies/:id/assignmentsApplicability rulesatt.policy.manage
GET/att/policies/:id/applicability-previewWho this policy applies to on a dateatt.policy.manage
POST/att/policies/:id/simulateSimulate recomputed days before publishingatt.policy.manageJob resource
GET / POST/att/devicesBiometric devicesatt.device.manage
PATCH/att/devices/:idEdit or rotate the device secretatt.device.manage
GET / PUT/att/devices/:id/user-mapsDevice user ID ↔ employee mappingatt.device.manageEffective-dated
GET / POST/att/period-locksLocks; start a lock with pre-lock checksatt.period.lockReturns blockers
POST/att/period-locks/:id/unlockReopen a period (while the pay run is not locked)att.period.lockReason required
GET/att/adjustmentsPost-lock adjustments awaiting payrollatt.record.read
POSTingest:/devices/:serial/punchesDevice push (ADMS-style)att.punch.ingest (device secret)On ingest.spyrl.app
GETingest:/devices/:serial/commandsDevice polls for pending commands (time sync, user upload)att.punch.ingest

7. MOB: mobile and kiosk​

MethodPathPurposePermissionNotes
GET/mob/configNavigation groups, entitlements, minimum version, cached fencesAuthenticatedCalled at app start
POST/mob/installsRegister an app installAuthenticated
PUT/mob/installs/:id/push-tokenUpdate the Expo push tokenAuthenticated
POST/mob/syncUpload 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/kiosksKiosk devices for locationsmob.kiosk.enroll
POST/mob/kiosks/:id/enrolment-codeIssue a one-time QR enrolment codemob.kiosk.enroll
POST/mob/kiosks/enrollTablet exchanges the QR code for a kiosk tokenEnrolment code
POST/mob/kiosks/:id/revokeRevoke a kioskmob.kiosk.enroll
GET/mob/kiosks/rosterLocation roster for offline identificationmob.kiosk.operateNames, codes, thumbnails, salted PIN hashes
POST/mob/kiosks/punchKiosk punch (face or PIN)mob.kiosk.operate + att.punch.create_on_behalfLiveness and match scores stored
GET / POST/mob/face-profilesFace enrolment status; enrolatt.face.enrollConsent required
DELETE/mob/face-profiles/:idDelete a face profileatt.face.enrollEmits mob.face.deleted

8. LVE: leave​

MethodPathPurposePermissionNotes
GET / POST/lve/typesLeave typeslve.balance.read / lve.type.manage
POST/lve/types/from-templateCreate a type from a templatelve.type.manage
PATCH/lve/types/:idEdit or deactivatelve.type.manage
GET / POST/lve/policiesLeave policieslve.policy.manageSame version and assignment sub-resources as ATT policies
POST/lve/policies/:id/versions/:versionId/publishPublish a versionlve.policy.managetransitionMode for existing balances
POST/lve/policies/:id/simulateSimulate accruals for employeeslve.policy.manage
GET/lve/balancesBalances by employee and type, as of a datelve.balance.readPublic
GET/lve/ledger-entriesLedger drill-downlve.balance.read
POST/lve/ledger-entriesManual adjustment with a reasonlve.balance.adjustEmits lve.ledger.adjusted
POST/lve/requests/quoteDay count, sandwich breakdown and balance impact without savinglve.request.createSee §18.2
GET / POST/lve/requestsLeave requests; applylve.request.create (SELF) or lve.request.create_on_behalfPublic; lists are also visible with lve.request.approve in scope
GET/lve/requests/:idRequest detail with workflow statusRequester or approver in scope
PATCH/lve/requests/:idEdit while submittedRequester
POST/lve/requests/:id/withdrawWithdraw before a decisionRequester
POST/lve/requests/:id/cancelCancel approved future leave (starts a cancellation workflow)lve.request.cancel
GET/lve/calendarTeam leave calendarlve.balance.readScope-filtered
GET / POST/lve/comp-off-creditsComp-off credits; request a creditlve.balance.read / lve.request.createApproved through tasks (lve.compoff.approve)
GET/lve/optional-holidaysOptional holidays available to meorg.holiday.read
POST/lve/optional-holidays/:holidayId/opt-inOpt in (creates a request)lve.request.create
GET / POST/lve/encashmentsEncashment requestslve.request.createApproved through tasks (lve.encashment.approve)
GET/lve/cyclesLeave cycleslve.balance.read
POST/lve/cycles/:id/closeYear-end close (carry-forward, lapse, encashment)lve.policy.manageJob resource
POST/lve/opening-balances/importImport opening balanceslve.balance.adjustdryRun
GET/lve/payroll-bridgeLOP and encashment per employee for a pay periodpay.run.reviewConsumed by PAY

9. PAY: payroll​

MethodPathPurposePermissionNotes
GET / POST/pay/componentsPay componentspay.component.manage
PATCH/pay/components/:idEdit a componentpay.component.manage
GET / POST/pay/structuresSalary structure templatespay.structure.manage
GET / POST/pay/structures/:id/versionsStructure versions and linespay.structure.manage
POST/pay/structures/:id/versions/:versionId/activateActivate a versionpay.structure.manageImmutable afterwards
POST/pay/structures/previewCTC breakdown preview for a structure and CTCpay.compensation.updatePure engine call
GET/pay/compensationsCompensation historypay.compensation.readPublic (explicit scope required); SELF for employees
POST/pay/compensationsNew compensation from a date (increment, promotion)pay.compensation.updateBack-dated changes create arrears inputs; emits pay.compensation.changed
GET / PUT/pay/employee-statutory/:employeeIdUAN, ESIC number, PF election, PT and LWF state, tax regimepay.compensation.read / pay.compensation.updateEffective-dated
GET / POST/pay/schedulesPay schedulespay.policy.manage
GET/pay/periodsPay periodspay.run.review
POST/pay/periods/generateGenerate periods for a financial yearpay.policy.manage
GET / POST/pay/policiesPayroll policiespay.policy.manageSame version and assignment sub-resources as ATT policies
GET / POST/pay/statutory-registrationsEPF, ESI, PT, LWF and TAN registrationsorg.company.read / org.company.manage
GET/pay/statutory-packsAvailable statutory pack versions and change logspay.policy.manage
POST/pay/statutory-packs/:version/acceptAccept a pack version for this tenantpay.statutory_pack.accept
GET / POST/pay/inputsOne-time inputs (arrears, variable pay, deductions)pay.input.managePublic
DELETE/pay/inputs/:idRemove a pending inputpay.input.manage
POST/pay/inputs/importImport inputspay.input.managedryRun
GET / POST/pay/runsPay runs; create a runpay.run.review / pay.run.createSee §18.4
GET/pay/runs/:idRun summary, totals, warningspay.run.review
POST/pay/runs/:id/prepareCheck the attendance lock, build the employee set, validatepay.run.createJob resource
POST/pay/runs/:id/calculateCalculate (all or selected employees)pay.run.createJob resource; body employeeIds optional
GET/pay/runs/:id/employeesPer-employee resultspay.run.review
GET/pay/runs/:id/employees/:employeeIdLines, input snapshot, explanationpay.run.review
POST/pay/runs/:id/employees/:employeeId/holdHold or release one employee's salarypay.run.create
GET/pay/runs/:id/varianceVariance against the previous periodpay.run.review
POST/pay/runs/:id/reviewMark reviewed and submit for approvalpay.run.reviewStarts the approval workflow (pay.run.approve on the task)
POST/pay/runs/:id/verifyRe-run the engine on stored snapshots and compare hashespay.run.reviewAlso runs automatically before lock
POST/pay/runs/:id/lockLock: freeze snapshots, render payslipspay.run.lockMFA step-up; If-Match
POST/pay/runs/:id/mark-paidRecord bank payment confirmationpay.run.lockEmits pay.run.paid
POST/pay/runs/:id/reverseCreate a reversal run for a locked runpay.run.reverse
POST/pay/runs/:id/cancelCancel an unlocked runpay.run.create
POST/pay/bank-filesGenerate a bank payout file for a runpay.bank_file.generatePublic (GET of the file); job resource
POST/pay/statutory-filesGenerate ECR, ESIC, PT, LWF or 24Q filespay.statutory_file.generateJob resource
POST/pay/journal-exportsPayroll journal for Tally, Zoho Books or CSVpay.journal.exportPublic
GET/pay/payslipsPayslipspay.payslip.readSELF for employees
GET/pay/payslips/:id/downloadPayslip PDF (password-protected)pay.payslip.readCache-Control: no-store
GET / POST/pay/loansLoans and salary advancespay.loan.manage; employee self-request: pay.advance.request (SELF)
GET / PUT/pay/tax-declarations/:financialYearMy IT declaration (regime, items)pay.tax_declaration.submitSELF
POST/pay/tax-declarations/:id/submitSubmit declarationspay.tax_declaration.submitEmits pay.declaration.submitted
POST/pay/tax-proofsUpload a proofpay.tax_declaration.submit
POST/pay/tax-proofs/:id/verifyAccept or reduce a proofpay.tax_declaration.verify
GET/pay/tax-projectionProjected annual tax under each regimepay.payslip.reademployeeId=me
POST/pay/form16/publishPublish Form 16 for a financial yearpay.form16.publishR2

10. NTF: notifications and workflow​

MethodPathPurposePermissionNotes
GET/ntf/tasksMy approval inbox (filter by request type, status, due)ntf.inbox.readIncludes delegated tasks
GET/ntf/tasks/:idTask with subject summary and historyAssignee
POST/ntf/tasks/:id/approveApproveAssignee + the subject's approve keySee §18.3
POST/ntf/tasks/:id/rejectReject (comment required)Assignee + the subject's approve key
POST/ntf/tasks/:id/send-backReturn to the requester for changesAssignee + the subject's approve keyClaims and timesheets
POST/ntf/tasks/bulk-decideApprove or reject up to 500 tasksAs above, per taskPer-item results
POST/ntf/tasks/:id/reassignReassign a taskntf.workflow.manageAudited
GET/ntf/instances/:idWorkflow instance with steps and actionsRequester, approver or ntf.workflow.manage
GET / POST/ntf/workflowsWorkflow definitions per request typentf.workflow.manage
POST/ntf/workflows/:id/versionsDraft a new version (steps, conditions, SLA)ntf.workflow.manage
POST/ntf/workflows/:id/versions/:versionId/publishPublish from a datentf.workflow.manageIn-flight instances keep their version
GET / POST/ntf/delegationsDelegate approvalsntf.delegation.manageSELF for employees; others for admins
DELETE/ntf/delegations/:idEnd a delegationntf.delegation.manage
GET / PUT/ntf/templatesNotification templates per event, channel and localentf.template.managePreview endpoint: /ntf/templates/preview
GET / PUT/ntf/channelsChannels, sender identity, reminder schedulesntf.channel.manage
GET/ntf/notificationsMy in-app notificationsntf.inbox.read
POST/ntf/notifications/:id/readMark readntf.inbox.readAlso /ntf/notifications/read-all
GET / PUT/ntf/preferencesMy channel preferencesntf.inbox.readSecurity messages can't be disabled
GET / POST/ntf/webhooksWebhook subscriptionsiam.api_client.managePublic; see Events & webhooks

11. RPT: reporting​

MethodPathPurposePermissionNotes
GET/rpt/datasetsDatasets available to merpt.report.readSalary datasets need rpt.payroll_report.read
POST/rpt/datasets/:key/queryRun a dataset query (paged)rpt.report.readScope-filtered; sensitive fields masked
GET / POST/rpt/viewsSaved viewsrpt.report.readSharing needs the same scope for viewers
GET / POST/rpt/exportsExport a dataset or view to XLSX, CSV or PDFrpt.report.exportPublic; job resource
GET/rpt/exports/:idExport status and download linkrpt.report.exportPublic
GET / POST/rpt/schedulesScheduled email reportsrpt.report.schedule
DELETE/rpt/schedules/:idStop a schedulerpt.report.schedule
GET/rpt/dashboards/:personaPersona dashboard tilesrpt.dashboard.read

12. CLM: claims​

MethodPathPurposePermissionNotes
GET / POST/clm/categoriesExpense categoriesclm.policy.manageGET for all employees
GET / POST/clm/policiesExpense policies (limits, per-diem, mileage)clm.policy.manageSame version and assignment sub-resources as ATT policies
POST/clm/claims/checkPre-submit policy and duplicate checkclm.claim.createNo side effects
GET / POST/clm/claimsClaims; create a draft with linesclm.claim.createPublic (GET); lists also visible to approvers in scope
GET/clm/claims/:idClaim with lines and workflowOwner or approver in scope
POST/clm/claims/:id/linesAdd a lineclm.claim.createDraft or sent back only
PATCH/clm/claims/:id/lines/:lineIdEdit a lineclm.claim.create
DELETE/clm/claims/:id/lines/:lineIdRemove a lineclm.claim.create
POST/clm/claims/:id/submitSubmit for approvalclm.claim.createSee §18.5
POST/clm/claims/:id/withdrawWithdraw before a decisionclm.claim.create
PATCH/clm/claims/:id/lines/:lineId/verificationSet the verified amount or reject one lineclm.claim.verifyFinance step
GET / POST/clm/advancesTravel advancesclm.claim.createApproved through tasks
GET/clm/ledger-entriesAdvance and payout ledgerclm.claim.create (SELF) or clm.claim.pay
GET / POST/clm/payout-batchesPayout batches (payroll or bank file)clm.claim.pay
POST/clm/payout-batches/:id/confirmConfirm a batch as paidclm.claim.payEmits 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​

MethodPathPurposePermissionNotes
GET / POST/ext/separationsSeparations; submit a resignationext.resignation.submit (SELF)Lists visible to approvers in scope
GET/ext/separations/:idSeparation detailEmployee, approver or HR in scope
POST/ext/separations/:id/withdrawWithdraw a resignationext.resignation.submitBefore approval, or per policy
POST/ext/separations/:id/lwd-proposalsPropose a different last working dayext.resignation.approve or the employeeNegotiation history
POST/ext/terminationsInitiate a terminationext.termination.initiateOptional immediate access revocation
GET/ext/separations/:id/clearanceClearance itemsext.clearance.updateItems are emp_task rows
POST/ext/separations/:id/clearance/:taskId/completeClear an item, with recoveriesext.clearance.update
GET / PUT/ext/separations/:id/exit-interviewExit interviewThe employee (submit); HR (read)Restricted visibility
POST/ext/fnf-settlementsCompute the F&F settlement (creates an FNF pay run)ext.settlement.computeJob resource
GET/ext/fnf-settlements/:idSettlement statementext.settlement.compute or ext.settlement.approve
POST/ext/fnf-settlements/:id/approveApprove the settlementext.settlement.approve
POST/ext/separations/:id/lettersRelieving and experience lettersemp.letter.generate
GET / POST/ext/reasonsSeparation and termination reasonsext.master.manage

Resignation approval goes through /ntf/tasks with ext.resignation.approve.


14. ENG: engagement​

MethodPathPurposePermissionNotes
GET/eng/feedMy feed: announcements, recognitions, celebrationseng.announcement.read
GET / POST/eng/announcementsAnnouncements; create a drafteng.announcement.read / eng.announcement.publish
PATCH/eng/announcements/:idEdit a draft or scheduled announcementeng.announcement.publish
POST/eng/announcements/:id/publishPublish or scheduleeng.announcement.publishAudience expansion job
POST/eng/announcements/:id/stopStop a published announcementeng.announcement.publish
POST/eng/announcements/:id/acknowledgeAcknowledgeeng.announcement.read
GET/eng/announcements/:id/readsRead and acknowledgement reporteng.announcement.publish
GET / POST/eng/recognition-typesRecognition types and badgeseng.program.manage
GET / POST/eng/recognitionsRecognitions; send oneeng.recognition.give
POST/eng/recognitions/:id/reactionsReacteng.recognition.give

15. FFM: field force and CRM​

MethodPathPurposePermissionNotes
GET / POST/ffm/pipelinesPipelines and stagesffm.pipeline.manageTemplates available
GET / POST/ffm/opportunitiesOpportunities (leads)ffm.account.read / ffm.account.managePublic
PATCH/ffm/opportunities/:idEditffm.account.manage
POST/ffm/opportunities/:id/stageMove to a stageffm.account.manageEmits ffm.opportunity.stage_changed
POST/ffm/opportunities/:id/reassignReassign ownerffm.lead.assign
GET / POST/ffm/associatesClient associatesffm.account.read / ffm.account.manage
GET / POST/ffm/activitiesCalls, visits, follow-upsffm.account.read / ffm.account.manage
GET / POST/ffm/visitsVisits; check in at a clientffm.visit.read / ffm.visit.createPublic (GET); location verified against the account
POST/ffm/visits/:id/check-outCheck outffm.visit.create
GET / POST/ffm/beat-plansBeat and route plansffm.visit.read / ffm.beat.manage
POST/ffm/sessionsStart a tracking session (after punch-in, with consent)ffm.visit.create
POST/ffm/sessions/:id/endEnd trackingffm.visit.createAlso automatic at punch-out
POSTingest:/ffm/sessions/:id/pointsUpload location points (batched)ffm.visit.createOn the ingest host; C4 data
GET/ffm/tracking/liveLive team positions and batteryffm.tracking.readTEAM scope; audited
GET/ffm/sessions/:id/trailA past trailffm.tracking.readWithin the retention window
GET/ffm/daily-travelDaily travel summariesffm.travel.submit (SELF) or ffm.travel.approve
POST/ffm/daily-travel/:id/submitSubmit a day's kmsffm.travel.submit
POST/ffm/daily-travel/:id/reviseRevise kms with a reasonffm.travel.approveApproval through tasks; emits ffm.travel.verified
GET / POST/ffm/paymentsField payment collectionsffm.account.read / ffm.payment.collect
GET / POST/ffm/targetsSales targetsffm.target.manage

16. TSM: timesheets​

MethodPathPurposePermissionNotes
GET / POST/tsm/projectsProjectstsm.project.manageGET also for members
PATCH/tsm/projects/:idEdit a projecttsm.project.manage
GET / PUT/tsm/projects/:id/membersMembers, roles, allocationtsm.project.manage
GET / POST/tsm/tasksTaskstsm.project.manage
GET/tsm/timesheetsTimesheetstsm.timesheet.submit (SELF) or tsm.timesheet.approve
PUT/tsm/timesheets/:weekStartSave my weekly gridtsm.timesheet.submit
POST/tsm/timesheets/:id/submitSubmit for approvaltsm.timesheet.submitApproved through tasks (tsm.timesheet.approve)
GET/tsm/defaultersMissing timesheetstsm.timesheet.approve
POST/tsm/defaulters/notifyNudge defaulterstsm.timesheet.approve
GET / POST/tsm/period-locksLock a timesheet periodtsm.project.manage

17. LMS: learning​

MethodPathPurposePermissionNotes
GET/lms/catalogueCourses open or assigned to melms.course.read
GET / POST/lms/coursesCourses (author view)lms.course.manage
PATCH/lms/courses/:idEdit modules, lessons and assessmentslms.course.manageSCORM upload via /sys/files
POST/lms/courses/:id/publishPublishlms.course.manage
GET / POST/lms/assignment-rulesAutomatic assignment ruleslms.course.assign
GET / POST/lms/enrollmentsEnrolments; assign learnerslms.course.read / lms.course.assignPublic (GET)
POST/lms/enrollments/:id/progressRecord lesson progresslms.course.read
POST/lms/assessments/:id/attemptsSubmit an attemptlms.course.readAnswer keys never leave the server
POST/lms/attempts/:id/gradeGrade a free-text attemptlms.assessment.grade
GET/lms/certificates/:id/downloadCertificate PDFlms.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.