Approver Delegation (Vacation Substitutions)
This guide explains how the SpendOne Platform, the AI-based platform for indirect corporate spend, handles temporary approver substitutions when an approver is out of office. A vacation substitution reroutes approval tasks from an absent approver to a designated stand-in over a fixed date window. The same mechanism covers planned vacations, sick leave and short-notice handovers, with full audit history of who approved on whose behalf.
For admins. Substitutions are managed under Profile → Out of Office for self-service or Administration → Users → Out of Office for admin-managed entries. Each substitution has a substitute (any active user other than the caller, and not themselves on vacation), a start date and an end date, both inclusive and expressed in YYYY-MM-DD. Substitutions move through four statuses: SCHEDULED (created but not yet started), ACTIVE (within the date window), COMPLETED (window ended) and CANCELED. The candidate picker returns every active user except the caller; it is deliberately not narrowed by grant, because approving is a baseline capability every user holds. Self-substitution is rejected, and a substitute who is themselves on vacation is rejected with HTTP 409. Only SCHEDULED substitutions can be edited; once the window opens, the entry is read-only and must be canceled to be undone. Activation, deactivation and notification fan-out (notification-vacation-delegation-scheduled, -activated, -ended) are scheduled jobs and run automatically. Every approval action taken by a substitute is recorded in the audit log with delegated_from set to the original approver, so reporting can always reconstruct who acted on whose behalf.
For integrators. All substitution endpoints live under /vacation-substitutions (see the API reference) and require a bearer token. The full surface:
| Method | Endpoint | Purpose |
|---|---|---|
POST | /vacation-substitutions | Create for the authenticated user |
POST | /vacation-substitutions/admin/{user_code} | Admin creates on behalf of another user |
GET | /me/vacation-substitution | Current SCHEDULED or ACTIVE entry for caller |
GET | /vacation-substitutions/users/{user_code} | Same, for a specified user |
GET | /vacation-substitutions/substitute-candidates | Eligible substitutes for caller |
PUT | /vacation-substitutions/{code} | Edit substitute or dates while SCHEDULED |
DELETE | /vacation-substitutions/{code} | Cancel; if ACTIVE, returns delegated tasks to original approver |
A typical create call:
POST /vacation-substitutions
Content-Type: application/json
{
"substitute_code": "01HXXXXXXXXXXXXXXXXXXXXXX",
"start_date": "2026-04-01",
"end_date": "2026-04-15"
}
The response contains a VacationSubstitution with code, status (SCHEDULED immediately after create), user_code, substitute_code, the date window, and timestamps. Once the start date arrives, a scheduled job activates the entry, sets status=ACTIVE, marks activated_at, and bulk-delegates the original approver's open approval tasks to the substitute by setting approver_code = substitute_code and delegated_from = original_approver_code. On end-date, a deactivation job sets status=COMPLETED, marks deactivated_at and returns any still-pending delegated tasks to the original approver.
Interaction with the rules engine
The rules engine (/rules) emits approval plans with abstract approver references, for example cost_center_owner. Substitutions are applied after the plan is generated, when each ApprovalTask already has a concrete approver_code. The approval service calls the substitution checker for every task and, if the resolved approver is currently ACTIVE on a vacation, swaps approver_code to the substitute and stores the original in delegated_from. Two specific edge cases are handled inline:
- Substitute is the requester. If the active substitute happens to be the buyer who created the purchase request, the task is auto-approved instead of being assigned to the buyer (no self-approval). This is recorded as
auto_approval=trueon the task. - Nested substitution. If approver A delegates to B and B is also on vacation delegating to C, the system performs a single hop only: A's tasks go to B, and B's existing or new tasks go to C. There is no recursive resolution. Configure substitute candidates to avoid known overlapping vacations; a substitute who is themselves on vacation is rejected at create and update time with a 409
substitute-on-vacation, not filtered out of the candidate list.
In-flight tasks and cancellation
Creating a substitution does not retroactively rewrite tasks that were assigned before the window opened. Only tasks pending at activation time, plus any new tasks generated during the window, route to the substitute. Canceling an ACTIVE substitution via DELETE /vacation-substitutions/{code} calls the approval service's undelegation path, which finds pending tasks where approver_code = substitute_code and delegated_from = original_approver_code and reverts them: approver_code is restored to the original, delegated_from is cleared, and the change is written to the audit log with action_type=UNDELEGATED.
Approvals already taken by the substitute remain attributed to that substitute with delegated_from populated. They are not retroactively reassigned, since the action genuinely happened. Reporting queries that need "approved on behalf of" should join on delegated_from.
Conflict cases and error codes
The service returns RFC 9457 problem details for the cases below. All conflicts are HTTP 409.
| Condition | Problem type | Source |
|---|---|---|
| User already has an active or scheduled vacation | …/problems/vacation-already-exists | Only one substitution per user at a time |
| Substitute user is also on vacation | …/problems/substitute-on-vacation | Prevents nested delegation at create time |
| User cannot substitute for themselves | …/problems/self-substitution | Self-substitution rejection |
| Start date is in the past | …/problems/start-date-in-past | Backdating is not permitted. This one is a 400, not a 409 |
| End date before start date | 400 validation | Inclusive date range |
Update on non-SCHEDULED entry | 409 | Cancel and recreate instead |
Audit trail
Every delegated approval task carries delegated_from permanently. Audit log entries with object_type=approval and action_type=DELEGATED record the swap: old_values.approver_code is the original approver, and new_values carries the substitute plus delegated_from. Combine these with the audit log API to reconstruct the full delegation history of any plan.
For event payload shapes and full status semantics, refer to the API reference.