Withdraw a pending period-close request
accounting:close_periodWithdraws a period-close request that has been raised but not yet
approved, returning the period from pending_approval to open.
BOOKS-100 PERIOD-CLOSE-UNDO (2026-08-28). Before this endpoint the
only exits from pending_approval were (a) a second person APPROVING
the request, which closes the period — the opposite of what someone who
mis-clicked wants — or (b) a raw UPDATE. And pending_approval is a
posting-blocking state, so one stray click froze a month of the ledger
with no in-product undo. This is the Rule 25 api-v1 twin of the
Cognito-JWT POST /gl/periods/{id}/withdraw-close-request; both call the
same canonical handler, so authorization and guards are identical.
Authorization, two tiers (Rule 33 point 5 — self-service on your OWN record never requires the org permission):
- Floor —
gl.close_period, the permission that let you RAISE the request in the first place. Gating the route itself ongl.approve_closewould lock the REQUESTER out of taking back their own mis-click, which is the whole population this endpoint exists for. - Plus — if the caller is NOT
close_requested_by, they additionally needgl.approve_close. Cancelling someone else’s request is an approval-authority act. Returns 403only_requester_or_approver_can_withdraw.
Guards:
- Layer 1 isolation — the period is scoped to the API key’s
entity_id; cross-tenant attempts return 404. - State guard — only
pending_approvalperiods can be withdrawn. Returns 409period_not_in_pending_approvalwithcurrent_status. - Row lock — the state read takes
FOR UPDATE, and the UPDATE re-assertsstatus = 'pending_approval', so a concurrentapprove-closecannot slip between the test and the flip. A lost race returns 409period_state_changed. - Audit log (Rule 20) — an
activity_logrow records the prior state, the original requester, the withdrawing user, whether the withdrawer WAS the requester, and the optional reason. - Broadcast (Rule 13) —
gl.period.close_request_withdrawnis emitted, mirroring thegl.period.close_requestedevent it undoes.
Write surface is deliberately three columns and no more: status
back to open, plus close_requested_by and close_requested_at
cleared. close_approved_by / close_approved_at /
close_checksum_hex are NOT touched — on a pending_approval row they
are NULL by construction (approve-close writes them in the SAME
statement that flips status to closed), so writing them would be a
claim about state this handler has not measured.
Authorizations
API key issued per entity via Settings > Developers > API Keys.
Each key carries scopes (e.g. orders:read, products:write).
Bearer token format: Authorization: Bearer ark_live_ent_Test keys use ark_test_ent_. Both are issued per entity
via Settings > Developers > API Keys.
Path Parameters
Body
Optional free-text justification. Recorded verbatim in the activity_log description and metadata. No length validation.
"Raised the close request by mistake while reviewing the period."
Response
Close request withdrawn; period returned to open.
An accounting period. Closed periods refuse new journal entry postings (Rule A3). Year-end closing entries that roll revenue and expense activity into retained earnings are posted via a separate explicit endpoint (POST /v1/accounting_periods/{id}/close-fiscal-year). The monthly-period close does NOT by itself post a retained-earnings rollover JE. Verified against live dev RDS accounting.accounting_periods.

