---
title: "Force a terminal decision (approved / rejected / withdrawn)"
method: POST
path: "/v1/submissions/{id}/finalize"
tags: ["publish"]
---

# Force a terminal decision (approved / rejected / withdrawn)

`POST /v1/submissions/{id}/finalize`

Wave 8: terminal-decision write. Approval is only valid from
`stage3`. Rejection is allowed from any non-terminal stage.
Withdraw is the submitter's own affordance; admins can withdraw
on a submitter's behalf with `submissions.write`.

Requires `submissions.staged` capability. Approve / reject require
`submissions.write` scope. Withdraw on behalf of another submitter
requires `submissions.write` too.

## Path parameters

- `id` string, required

## Request body

- SubmissionFinalize
  - `decision` 'approved' | 'rejected' | 'withdrawn', required
  - `decision_reason` string, nullable

## Response `200`

Submission moved to terminal state

- Submission — Full submission row with the standardized `submissions.staged` check shape. The orchestrator's `routers/app_submissions.py` proxy mirrors this row directly into the local `app_submissions` cache so existing UI rendering keeps working unchanged.
  - `id` string, required — Submission UUID (string form)
  - `kind` 'agent' | 'skill' | 'mcp_server' | 'base' | 'app' | 'theme' | 'workflow_template', required
  - `slug` string, required
  - `version` string, nullable
  - `state` 'stage0_received' | 'stage1_static' | 'stage2_dynamic' | 'stage3_review' | 'approved' | 'rejected' | 'withdrawn', required — Wire-stable state string mirrored from the row
  - `stage` 'stage0' | 'stage1' | 'stage2' | 'stage3' | 'approved' | 'rejected' | 'withdrawn', required — Current stage; terminal stages mirror the decision
  - `decision` 'approved' | 'rejected' | 'withdrawn', nullable
  - `decision_reason` string, nullable
  - `submitter_handle` string, nullable
  - `bundle_sha256` string, nullable
  - `bundle_size_bytes` integer, nullable
  - `item_id` string, nullable
  - `item_version_id` string, nullable
  - `checks` SubmissionCheck[] — Append-only per-stage check rows. Clients with the `submissions.staged` capability render these grouped by `stage` to surface the staged pipeline timeline.
    - `stage` 'stage0' | 'stage1' | 'stage2' | 'stage3', required — Pipeline stage that produced this check
    - `name` string, required — Stable check identifier (e.g. `slug_format`, `bundle_size`)
    - `status` 'passed' | 'failed' | 'warning' | 'errored' | 'skipped', required — * `passed` — check succeeded * `failed` — check failed; submission transitions to rejected * `warning` — informational; does not fail the stage * `errored` — execution itself failed; submission transitions to rejected * `skipped` — check did not run (e.g. dependent check failed)
    - `message` string, nullable
    - `details` object, nullable
    - `created_at` string, date-time, required
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `400` — Missing or invalid decision
- `404` — Not found
- `409` — Invalid transition or already terminal
- `501` — Capability disabled on this hub

---

[API](https://skmtc.net/tesslateai/apis/tesslate-federated-marketplace-v1.md) · [All operations](https://skmtc.net/tesslateai/apis/tesslate-federated-marketplace-v1/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/tesslateai/tesslate-federated-marketplace-v1/revisions/b2706e1525df/schema)
