# Submit a model

```http
POST /api/public/v1/competitions/modular-arithmetic-challenge/submissions
Content-Type: application/json
```

**Scope**: `competition.write`.

Creates or replaces one numbered model-reference slot for the caller's active competition team. Only the team owner may write a slot, and [Get my participation](./get-my-participation.md) must currently return `canSubmit: true`.

Before submitting, read [Get competition detail](./get-competition-detail.md) for the live slot limit and submission window. Push the selected revision to a public Hugging Face model repository; SAIR verifies the repository and exact commit before storing the reference.

## Request body

```ts
type SubmitModelReferenceRequest = {
  idempotencyKey: string;
  slot?: number;
  payload: {
    modelName: string;
    commitHash: string;
  };
  meta?: {
    description?: string;
  };
};
```

| Field | Required | Description |
| :---- | :------: | :---------- |
| `idempotencyKey` | Yes | Non-empty client-generated retry key, at most 200 UTF-8 bytes after surrounding whitespace is removed. |
| `slot` | No | Integer from `1` through the live `submissionSpec.limits.maxEntries`. Omitting it selects slot `1`; the API never selects the first empty slot. |
| `payload.modelName` | Yes | Public Hugging Face model repository in the `owner/name` form required by the live `submissionSpec.schema`. |
| `payload.commitHash` | Yes | Full 40-character commit SHA for the exact repository revision. Branch names, tags, and abbreviated SHAs are not accepted. |
| `meta.description` | No | Human-readable note of at most 5,000 characters. |

Each slot is independent. Writing an occupied slot replaces that slot in place and leaves every other slot unchanged. The slot retains its stable `submissionId` across replacements.

## Idempotency

Generate one opaque `idempotencyKey` for each intended create or replacement operation. Reuse that key only when retrying the same resolved slot, model reference, and metadata after a timeout or uncertain response.

An exact replay returns the original operation's recorded response with `200 OK`. It does not verify the Hugging Face revision again, update timestamps, or write the slot again. Reusing the key with different content returns `409 IDEMPOTENCY_CONFLICT`; use a new key for a deliberate replacement.

## Example request

```bash
export SAIR_IDEMPOTENCY_KEY="mac-submit-$(openssl rand -hex 16)"

curl -X POST "https://api.sair.foundation/api/public/v1/competitions/modular-arithmetic-challenge/submissions" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "'"$SAIR_IDEMPOTENCY_KEY"'",
    "slot": 2,
    "payload": {
      "modelName": "your-team/your-model",
      "commitHash": "0123456789abcdef0123456789abcdef01234567"
    },
    "meta": {
      "description": "Pinned model revision."
    }
  }'
```

## Response

The first accepted write to an empty slot returns `201 Created`. A new logical operation that replaces an occupied slot returns `200 OK` and retains the slot's `submissionId`. An exact idempotency replay also returns `200 OK` without changing the stored entry or its timestamps.

```json
{
  "ok": true,
  "data": {
    "submissionId": "sub_01JMAC00000000000000000002",
    "competitionId": "modular-arithmetic-challenge",
    "kind": "model-reference",
    "slot": 2,
    "payload": {
      "modelName": "your-team/your-model",
      "commitHash": "0123456789abcdef0123456789abcdef01234567"
    },
    "meta": {
      "description": "Pinned model revision."
    },
    "createdAt": "2026-05-18T12:34:56Z",
    "updatedAt": "2026-05-20T08:11:02Z"
  }
}
```

`createdAt` is when the stable slot entry was first created. `updatedAt` changes only when a new logical replacement is accepted. Use [List my submissions](./list-my-submissions.md) to read the team's current occupied slots; SAIR stores model references, not downloadable model files.

## Errors

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | The JSON body, content type, or top-level request shape is invalid. |
| `400` | `IDEMPOTENCY_KEY_REQUIRED` | `idempotencyKey` is missing or empty. |
| `400` | `IDEMPOTENCY_KEY_TOO_LONG` | `idempotencyKey` exceeds 200 UTF-8 bytes. |
| `403` | `EMAIL_NOT_VERIFIED` | The caller's email is not verified. |
| `403` | `ENROLL_REQUIRED` | The caller has not completed enrollment. |
| `403` | `TEAM_REQUIRED` | The caller does not currently have an active competition team. |
| `403` | `TEAM_OWNER_REQUIRED` | The caller is an active team member rather than its owner. |
| `403` | `SUBMISSION_WINDOW_NOT_OPEN` | The formal submission window has not opened. |
| `403` | `SUBMISSION_WINDOW_CLOSED` | The formal submission window has closed. |
| `404` | `NOT_FOUND` | The competition does not exist or is not publicly visible. |
| `409` | `IDEMPOTENCY_CONFLICT` | The same key was already used with a different request. |
| `422` | `SUBMISSION_PAYLOAD_INVALID` | The model repository or commit is missing, malformed, private, not found, or does not match the live payload schema. |
| `422` | `SUBMISSION_META_INVALID` | `meta` is invalid or contains an unknown field. |
| `422` | `SUBMISSION_SLOT_INVALID` | `slot` is outside the live range. |
| `503` | `MODEL_REFERENCE_LOOKUP_UNAVAILABLE` | Hugging Face could not be reached or returned a transient error during verification. Retry the same request with the same `idempotencyKey`. |

See [Errors](../../../errors.md) for authentication, scope, rate-limit, and standard error-envelope behavior.
