# Submit polynomials

```http
POST /api/public/v1/competitions/igp24/submissions
Content-Type: application/json
```

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

Creates an append-only IGP24 validation record for the caller's immutable deadline team. Well-formed polynomial lines enter asynchronous validation; malformed lines are reported independently. In the current post-event mode, a validated result may establish a Discovery record but can never change the frozen official score, rank, awards, or results.

The caller must have a verified email and be eligible under the live `submissionSpec.submissionStatus` returned by [Get competition detail](./get-competition-detail.md). `maySubmit` and [Get my participation](./get-my-participation.md)'s `canSubmit` are preflight values, not reservations; this endpoint repeats admission checks when it accepts a new batch.

The compatibility path `/api/public/v1/competitions/igp24/playground/submissions` has the same contract. New integrations should use the canonical path above.

## Request body

```ts
type SubmitIgp24PolynomialsRequest = {
  idempotencyKey: string;
  payload: {
    polynomials: string[];
  };
  meta?: {
    description?: string;
  };
};
```

| Field | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `idempotencyKey` | string | Yes | Client-generated key for this logical submission. After surrounding whitespace is removed, it must contain at least one character and be at most 200 UTF-8 bytes. |
| `payload.polynomials` | string[] | Yes | One or more polynomial lines, up to `submissionSpec.limits.maxPolynomials`. Order and duplicate positions are preserved in results. |
| `meta.description` | string | No | Stored batch note. Its maximum length is `submissionSpec.metaSchema.properties.description.maxLength`; it does not affect validation or Discovery attribution. |

Read the live item and byte limits from [Get competition detail](./get-competition-detail.md). The UTF-8 byte length of the polynomial strings joined with `\n` must not exceed `submissionSpec.limits.maxBytes`. The normalized JSON request must also fit within `submissionSpec.limits.maxRequestBytes`.

### Idempotency

Generate one opaque `idempotencyKey` for each intended persistent batch. Reuse that key only when retrying the same `payload` and `meta` after a timeout or uncertain response. Polynomial order, duplicate positions, whitespace inside each string, comments, and hint text are part of the operation.

The key is scoped to the IGP24 competition and deadline team:

- The first accepted operation returns `201 Created`.
- Replaying the same key with the same request returns the original creation result with `200 OK`. It does not create another history record or batch, enqueue work again, or consume another team-budget unit—even if the mode later closes or the team has since exhausted its daily budget.
- Reusing the key with different `payload` or `meta` returns `409 IDEMPOTENCY_CONFLICT`.

Authentication and deadline-team authorization are still checked on every replay.

### Polynomial line format

Each line contains exactly 25 comma-separated decimal integers in coefficient order `a_0,a_1,...,a_24`. The constant coefficient `a_0` must be nonzero and `a_24` must be `1`, so the polynomial is monic of degree 24.

A `#` starts a trailing comment. Ordinary comments are preserved in the downloadable source but ignored by validation. The reserved `poly_disc_primes=[...]` hint accepts known prime factors as strictly increasing decimal integers greater than `1`; an invalid hint is ignored without rejecting an otherwise valid polynomial.

The hint can accelerate the official-period discriminant pipeline when that pipeline is active. The current post-event validator does not calculate a scoring discriminant, so the hint does not affect current validation, Discovery attribution, or any official result.

## Example request

```bash
SAIR_IDEMPOTENCY_KEY="igp24-$(openssl rand -hex 16)"

curl -X POST "https://api.sair.foundation/api/public/v1/competitions/igp24/submissions" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "'"$SAIR_IDEMPOTENCY_KEY"'",
    "payload": {
      "polynomials": [
        "3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 # poly_disc_primes=[2,3]",
        "2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1",
        "1,2,3 # malformed example"
      ]
    },
    "meta": {
      "description": "Search batch 12."
    }
  }'
```

## Admission and processing

The endpoint classifies every request position before returning:

- A malformed line is added to `failedPolynomials` and does not prevent other positions from entering the batch.
- In post-event mode, an exact polynomial already present in the published Open Data release may be added to `filteredPolynomials` with `reasonCode: "ALREADY_PUBLISHED_IGP24_POLYNOMIAL"`. It is terminal, not rejected, and is not evaluated again.
- Remaining well-formed positions are added to `payload.queuedPolynomials`. Canonically identical positions are evaluated once and later fan out to every original `polynomialIndex`.

When at least one well-formed position is admitted—including a position filtered as already published—the operation consumes one unit from the deadline team's shared UTC-day batch budget. Batch size, duplicates, and filtered positions do not change that charge. An exact idempotency replay consumes no additional unit.

If every position is malformed, the endpoint still creates the append-only history record so the caller can inspect and download the attempted input. No validation batch is created and no team-budget unit is consumed.

Validation runs asynchronously. Save the returned identifiers and poll for results; a queued batch does not need to be resubmitted when evaluation is delayed.

## Response

`polynomialIndex` is the zero-based position in the original request. Submitted coefficient strings are intentionally omitted from JSON responses; use [Download a submission](./download-submission.md) to retrieve the original lines.

```ts
type SubmitIgp24PolynomialsResponse = {
  ok: true;
  data: {
    submissionId: string;
    competitionId: "igp24";
    kind: "igp24-polynomial";
    payload: {
      queuedPolynomials: Array<{
        polynomialIndex: number;
        status: "queued";
      }>;
    };
    meta: {
      description?: string;
    };
    verifiedPolynomials: [];
    filteredPolynomials: Array<{
      polynomialIndex: number;
      status: "filtered";
      reasonCode: "ALREADY_PUBLISHED_IGP24_POLYNOMIAL";
      message: string;
    }>;
    failedPolynomials: Array<{
      polynomialIndex: number;
      status: "rejected";
      reason: string;
    }>;
    createdAt: string;
    updatedAt: string;
    description: string | null;
    batchId?: string;
    submissionStatus: "queued" | "complete";
    submittedCount: number;
    filteredPublishedCount: number;
    queuedForEvaluationCount: number;
    queuedCount: number;
    rejectedCount: number;
    sourcePhase: "competition" | "discovery_only";
    officialScoringEligible: boolean;
  };
};
```

### Summary fields

| Field | Description |
| :--- | :--- |
| `batchId` | Batch to poll. Present when at least one well-formed position was admitted, including an all-filtered batch; absent when every position was malformed. |
| `submissionStatus` | `queued` while any distinct polynomial awaits validation; otherwise `complete`. |
| `submittedCount` | Well-formed request positions, including duplicates and filtered positions. It excludes malformed positions. |
| `filteredPublishedCount` | Positions terminally matched to the published Open Data release. |
| `queuedForEvaluationCount` | Distinct canonical polynomials actually sent to asynchronous validation. |
| `queuedCount` | Original request positions represented by queued work, including duplicates. |
| `rejectedCount` | Positions rejected before batch validation. `submittedCount + rejectedCount` equals the original array length. |
| `sourcePhase` | Server-side phase frozen at first acceptance. The current post-event wire value is `discovery_only`. |
| `officialScoringEligible` | Whether this batch can affect official scoring. It is permanently `false` for `sourcePhase: "discovery_only"`. |

`verifiedPolynomials` is empty in the creation response because validation does not run inline. Save `submissionId` for history and downloads. When `batchId` is present, poll [Get batch status](./get-batch-status.md).

## Example response

The filtered position and IDs below illustrate the post-event response shape. Use the values returned by the endpoint.

```json
{
  "ok": true,
  "data": {
    "submissionId": "sub_4fa85f64f18d4dc59eea4d2ef65db490",
    "competitionId": "igp24",
    "kind": "igp24-polynomial",
    "payload": {
      "queuedPolynomials": [
        {
          "polynomialIndex": 1,
          "status": "queued"
        }
      ]
    },
    "meta": {
      "description": "Search batch 12."
    },
    "verifiedPolynomials": [],
    "filteredPolynomials": [
      {
        "polynomialIndex": 0,
        "status": "filtered",
        "reasonCode": "ALREADY_PUBLISHED_IGP24_POLYNOMIAL",
        "message": "This polynomial is already included in the published IGP24 dataset and was not evaluated."
      }
    ],
    "failedPolynomials": [
      {
        "polynomialIndex": 2,
        "status": "rejected",
        "reason": "expected 25 coefficients, found 3"
      }
    ],
    "createdAt": "2026-08-31T12:34:56Z",
    "updatedAt": "2026-08-31T12:34:56Z",
    "description": "Search batch 12.",
    "batchId": "igp24_lab_batch_8c1f932a72984ff9b51a63c1a62fd997",
    "submissionStatus": "queued",
    "submittedCount": 2,
    "filteredPublishedCount": 1,
    "queuedForEvaluationCount": 1,
    "queuedCount": 1,
    "rejectedCount": 1,
    "sourcePhase": "discovery_only",
    "officialScoringEligible": false
  }
}
```

If every line is malformed, the endpoint returns `201 Created` with `submissionStatus: "complete"`, `submittedCount: 0`, `queuedCount: 0`, and every request position in `failedPolynomials`. `batchId` is omitted. Replaying that operation with the same idempotency key returns the same record with `200 OK`.

Request-level shape errors remain request errors rather than per-line results. A missing, empty, or over-limit `payload.polynomials` array returns `422 SUBMISSION_PAYLOAD_INVALID`; an over-limit description returns `422 SUBMISSION_META_INVALID`; an over-limit body returns `413 FILE_TOO_LARGE`.

## Quota and retries

This endpoint does not consume the verify-only anti-abuse quota. Each newly admitted persistent batch consumes one unit from the deadline team's shared budget reported by `submissionSpec.submissionStatus`; `429 TEAM_DAILY_LIMIT` includes quota metadata and retry headers for the next UTC reset.

After a timeout, lost response, or transient `5xx`, retry the exact request with the same `idempotencyKey` and bounded backoff. Using a new key creates a new logical operation and may consume another team-budget unit.

## Errors

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | The JSON body, `Content-Type`, top-level shape, or a field type is invalid. |
| `400` | `IDEMPOTENCY_KEY_REQUIRED` | `idempotencyKey` is missing or empty after trimming. |
| `400` | `IDEMPOTENCY_KEY_TOO_LONG` | `idempotencyKey` exceeds 200 UTF-8 bytes after trimming. |
| `403` | `EMAIL_NOT_VERIFIED` | The account email is not verified. |
| `403` | `OFFICIAL_STORE_FROZEN` | This service instance exposes only the frozen official store and cannot accept new batches. |
| `403` | `DISCOVERY_ONLY_CLOSED` | Post-event validation is disabled or has ended. |
| `403` | `DISCOVERY_ONLY_NOT_ELIGIBLE` | The caller has no eligible deadline-team participation. |
| `403` | `DISCOVERY_ONLY_TEAM_DISQUALIFIED` | The deadline team is not permitted to submit post-event batches. |
| `404` | `NOT_FOUND` | IGP24 does not exist or is not publicly visible. |
| `409` | `IDEMPOTENCY_CONFLICT` | The same idempotency key was already used with different `payload` or `meta`. |
| `413` | `FILE_TOO_LARGE` | The joined polynomial text or normalized JSON request exceeds its published byte limit. |
| `422` | `SUBMISSION_PAYLOAD_INVALID` | `payload` fails the published schema, including a missing, empty, or over-limit `polynomials` array. |
| `422` | `SUBMISSION_META_INVALID` | `meta` fails its published schema or `description` exceeds its limit. |
| `429` | `TEAM_DAILY_LIMIT` | The deadline team exhausted its current UTC-day persistent batch budget. |
| `503` | `IGP24_OPEN_DATA_FILTER_UNAVAILABLE` | The published-data check is temporarily unavailable. No batch was accepted; retry with the same idempotency key. |
| `503` | `IGP24_SERVICE_UNAVAILABLE` | Submission processing is temporarily unavailable. Retry the same operation with the same idempotency key. |

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