# Save a Playground solution

```http
POST /api/public/v1/competitions/lean-kernel-challenge/playground/solutions
Content-Type: application/json
```

**Scope**: `playground.write`.

Creates an owned private named Solution, or replaces the current state of one exact saved Solution revision. Saving does not create a practice Run, formal submission, or Contributor Network item.

The caller must be enrolled, the Playground must be open for new work, and `problemId` must identify a currently available Problem. A Competition Team is not required. Existing Solutions remain readable when any of those write conditions later stops being true.

## Create and update behavior

| Operation | Identity fields | Result |
| :--- | :--- | :--- |
| Create | Omit both `solutionId` and `revision`. | Creates a new Solution at revision `1` and returns `200 OK`. |
| Update | Send both the owned `solutionId` and its exact current `revision`. | Replaces `name` and the complete Workspace, increments `revision` by one, and returns `200 OK`. |

An update preserves `solutionId`, `problemId`, and `createdAt`. It cannot move a Solution to another Problem. `updatedAt` changes only after the replacement commits successfully.

## Request fields

| Field | Type, requirement, and meaning |
| :--- | :--- |
| `solutionId` | **Conditionally required string.** Stable opaque ID returned by [List saved solutions](./list-playground-solutions.md). Send it only when updating, together with `revision`; pass it through unchanged. |
| `revision` | **Conditionally required integer.** Exact current positive revision used for optimistic concurrency. Send it if and only if `solutionId` is present. |
| `problemId` | **Required string.** Exact, case-sensitive ID of a currently `available` Problem from [List problems](./list-problems.md). On update it must equal the Solution's immutable `problemId`. |
| `name` | **Required string.** After surrounding Unicode whitespace is removed, the stored display name must contain `1` through `200` UTF-8 bytes. |
| `files` | **Required `Record<string, string>`.** Complete replacement Workspace. It must contain non-empty root `Submission.lean`; auxiliary files may use canonical paths below `Submission/`. |

On update, every path omitted from `files` is deleted. Read the current Solution first and preserve every auxiliary file that you do not intentionally remove.

### Name uniqueness

Names are unique within the authenticated account, Competition, and Problem. For conflict detection, the server trims surrounding whitespace, applies Unicode NFKC normalization, and performs Unicode case folding. The returned `name` keeps the caller's trimmed spelling; the normalized comparison value is not returned.

Creating another Solution with an equivalent name, or renaming an existing Solution to one, returns `409 SOLUTION_NAME_CONFLICT`. It never overwrites the other Workspace.

### Workspace limits

- `Submission.lean` must contain at least one byte and must not exceed 1 MiB (1,048,576 UTF-8 bytes).
- The complete Workspace must contain from `1` through `256` files and at most 8 MiB (8,388,608 UTF-8 content bytes).
- Every auxiliary path must begin with `Submission/`. Paths are relative, use `/`, contain no empty, `.` or `..` segment, and have no leading or trailing whitespace.
- A path may contain at most 4,096 UTF-8 bytes, and each segment at most 255 UTF-8 bytes. Absolute paths, drive-letter paths, backslashes, NUL, duplicate names, and file/directory collisions are rejected.
- Every file value must be a non-empty JSON string. The size limits count decoded UTF-8 content bytes, not filenames or JSON encoding overhead.

When editing a Workspace, preserve auxiliary files returned by the API unless you intend to remove them. A [formal submission](./submit-package.md) copies only `Submission.lean` from the saved Solution.

## Concurrency and uncertain responses

The `revision` check prevents a concurrent update from silently overwriting newer content. A stale update returns `409 REVISION_CONFLICT` without changing the Solution. Reload the complete Solution, reconcile the files, and then submit its new current revision.

This endpoint does not use an idempotency key. Do not automatically retry a create or update after a timeout or other uncertain response. Read the current Solution list first: normalized-name uniqueness prevents duplicate creates, and the revision tells you whether an update was saved.

## Create example

```bash
export SAIR_API_BASE="https://api.sair.foundation/api/public/v1"

curl -X POST \
  "$SAIR_API_BASE/competitions/lean-kernel-challenge/playground/solutions" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "problemId": "fib",
    "name": "baseline",
    "files": {
      "Submission.lean": "import Spec\n\nnamespace Submission\n\ndef impl : Nat → Nat := fibSpec\n\ntheorem impl_correct : ∀ n, impl n = fibSpec n := fun _ => rfl\n\nend Submission\n"
    }
  }'
```

## Create response

```json
{
  "ok": true,
  "data": {
    "solutionId": "17",
    "problemId": "fib",
    "name": "baseline",
    "files": {
      "Submission.lean": "import Spec\n\nnamespace Submission\n\ndef impl : Nat → Nat := fibSpec\n\ntheorem impl_correct : ∀ n, impl n = fibSpec n := fun _ => rfl\n\nend Submission\n"
    },
    "revision": 1,
    "published": false,
    "publicItemId": null,
    "createdAt": "2026-09-04T11:30:00Z",
    "updatedAt": "2026-09-04T11:30:00Z"
  }
}
```

## Update example

Send the complete replacement Workspace, not only the file that changed:

```bash
curl -X POST \
  "$SAIR_API_BASE/competitions/lean-kernel-challenge/playground/solutions" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "solutionId": "17",
    "revision": 1,
    "problemId": "fib",
    "name": "baseline-reviewed",
    "files": {
      "Submission.lean": "import Spec\n\nnamespace Submission\n\ndef impl : Nat → Nat := fibSpec\n\ntheorem impl_correct : ∀ n, impl n = fibSpec n := fun _ => rfl\n\nend Submission\n"
    }
  }'
```

## Update response

```json
{
  "ok": true,
  "data": {
    "solutionId": "17",
    "problemId": "fib",
    "name": "baseline-reviewed",
    "files": {
      "Submission.lean": "import Spec\n\nnamespace Submission\n\ndef impl : Nat → Nat := fibSpec\n\ntheorem impl_correct : ∀ n, impl n = fibSpec n := fun _ => rfl\n\nend Submission\n"
    },
    "revision": 2,
    "published": false,
    "publicItemId": null,
    "createdAt": "2026-09-04T11:30:00Z",
    "updatedAt": "2026-09-04T11:45:00Z"
  }
}
```

## Response fields

The success envelope's `data` is one [`SavedSolution`](./list-playground-solutions.md#savedsolution):

| Field | Type and meaning |
| :--- | :--- |
| `solutionId` | `string` — Stable opaque Solution ID. |
| `problemId` | `string` — Immutable Problem ID. |
| `name` | `string` — Trimmed stored display name. |
| `files` | `Record<string, string>` — Complete saved Workspace. |
| `revision` | `integer` — Current positive optimistic-concurrency revision. |
| `published` | `boolean` — Whether the returned Solution revision has an active Contributor Network publication. A successful create or update returns `false`. |
| `publicItemId` | `string \| null` — Active Contributor Network item ID when published; a successful create or update returns `null`. |
| `createdAt` | `string` — ISO 8601 UTC creation time. |
| `updatedAt` | `string` — ISO 8601 UTC time when this create or update was saved. |

Successful responses use `Content-Type: application/json`.

Use the returned `solutionId` and `revision` to [create a practice Run](./create-playground-run.md) or [publish an immutable Solution snapshot](./publish-contributor-package.md).

## Errors

| Error | When |
| :--- | :--- |
| `400 INVALID_JSON_BODY` | The JSON body or a field type is invalid. |
| `403 ENROLL_REQUIRED` | The caller is not enrolled in the Competition. |
| `403 PLAYGROUND_NOT_STARTED` | The Playground has not opened for new work. |
| `403 PLAYGROUND_READ_ONLY` | The Playground is closed or disabled for new work. |
| `404 NOT_FOUND` | The Competition is not visible, or the Solution being updated does not exist or is not owned by the caller. |
| `409 SOLUTION_NAME_CONFLICT` | Another owned Solution has the same normalized name for this Problem. |
| `409 SOLUTION_LIMIT_REACHED` | The account already has the configured maximum number of Solutions for this Problem. `details` contains `problemId`, `limit`, and `currentCount`. |
| `409 REVISION_CONFLICT` | The saved Solution changed after the supplied revision was read. Reload it before reconciling and retrying. |
| `409 SOLUTION_PROBLEM_IMMUTABLE` | The update attempts to move a Solution to another Problem. |
| `409 SOLUTION_PUBLICATION_LOCKED` | The Solution is in `publishing` or `published` state. `details.publicationState` identifies the lock. |
| `413 FILE_TOO_LARGE` | `Submission.lean` exceeds 1 MiB. |
| `413 WORKSPACE_TOO_LARGE` | All Workspace file contents together exceed 8 MiB. |
| `422 RESOURCE_FIELD_INVALID` | The Problem is unavailable, the name is invalid, or the Workspace violates a file-count, content, path, duplicate-name, or tree constraint. |
| `500 SOLUTION_STORE_FAILED` | The Solution could not be saved or its saved state confirmed. Check your saved Solutions before retrying. |
| `502 PLATFORM_UNAVAILABLE` | Competition visibility, lifecycle, or enrollment cannot be verified. |
| `502 PLAYGROUND_CONFIGURATION_INVALID` | The authoritative Playground lifecycle configuration is invalid. |
| `503 SOLUTION_STORE_BUSY` | The Solution store is temporarily busy; read the current Solution before deciding whether to retry. |

See [Errors](../../../errors.md) for shared authentication, scope, and rate-limit errors.
