# Verify a polynomial batch

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

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

Synchronously verifies the degree and Galois label of one or more degree-24 polynomials. It does not persist polynomial content, create a submission or batch, consume the deadline team's persistent submission budget, or affect an official discovery, score, rank, award, or result.

The caller does not need to be enrolled or belong to an IGP24 team. The competition must still be publicly visible, and every batch accepted for processing consumes the verification-only anti-abuse quota described below. The compatibility path `/api/public/v1/competitions/igp24/playground/verify/batch` has the same contract; new integrations should use the canonical path above.

## Request body

```ts
type VerifyIgp24PolynomialsRequest = {
  polynomials: string[];
  description?: string;
};
```

| Field | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `polynomials` | string[] | Yes | One or more polynomial lines, up to `submissionSpec.limits.maxPolynomials` from [Get competition detail](./get-competition-detail.md). |
| `description` | string | No | Note echoed as `data.description` but not stored with a submission. Its maximum length is `submissionSpec.metaSchema.properties.description.maxLength`. |

The UTF-8 byte length of the lines joined with `\n` must not exceed `submissionSpec.limits.maxBytes`. The complete normalized JSON request must also fit within `submissionSpec.limits.maxRequestBytes`.

### 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 ignored. The reserved `poly_disc_primes=[...]` hint is also ignored by this endpoint because dry-run verification computes the Galois label but not a scoring discriminant; the hint is relevant only to [Submit polynomials](./submit-polynomials.md).

## Example request

```bash
curl -X POST "https://api.sair.foundation/api/public/v1/competitions/igp24/verify/batch" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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",
      "1,2,3"
    ],
    "description": "Candidate batch 12."
  }'
```

## Response

`results` has one entry for every request position and preserves request order, including duplicates. Use the array index to associate a result with its input line.

- `accepted` means the polynomial passed degree-24 Galois verification and includes its computed `24Tt` label and real-root signature `r`. It does not mean the polynomial was submitted, persisted, discovered first, or awarded points.
- `rejected` includes a safe reason. A malformed line is rejected independently and does not prevent other lines from being verified. A batch containing only malformed lines still returns `200 OK` with one rejected result per position.

The dry run does not compute a scoring discriminant. During the official competition, a non-baseline accepted pair therefore uses `scoringStatus: "pending"`; a baseline pair may use `scoringStatus: "no_score"`. In post-event validation mode, every accepted result uses `scoringStatus: "no_score"`, `scoringReason: "post_event_validation_only"`, and `noScoreReason: "post_event_validation_only"`. It remains mathematically accepted but can never change the frozen official score or ranking.

`scoringStatus` and `scoringReason` are the authoritative scoring fields. `scoreable` is a legacy summary and remains `false` for this dry run because no scoring discriminant is calculated.

```ts
type VerifyIgp24PolynomialsResponse = {
  ok: true;
  data: {
    results: Array<
      | {
          status: "accepted";
          label: string;
          t: number;
          r: number;
          scoreable: false;
          scoringStatus: "pending" | "no_score";
          scoringReason:
            | "discriminant_pending"
            | "lmfdb_baseline"
            | "post_event_validation_only";
          noScoreReason?: string | null;
          reason?: string;
          inBaseline: boolean;
          baselineUnlocked: false;
          baselineDiscAbs?: string;
        }
      | {
          status: "rejected";
          reason: string;
        }
    >;
    description: string | null;
  };
};
```

### Accepted-result fields

| Field | Type | Description |
| :--- | :--- | :--- |
| `label` | string | Computed canonical transitive-group label `24Tt`. |
| `t` | integer | Numeric transitive-group position encoded by `label`. |
| `r` | integer | Computed real-root signature. |
| `scoreable` | `false` | Legacy summary. Dry-run verification never calculates the discriminant needed to produce a scoreable verdict. |
| `scoringStatus` | `pending` \| `no_score` | Whether a later persisted official submission still needs discriminant evaluation or cannot score in the current context. |
| `scoringReason` | string enum | Stable machine-readable reason for `scoringStatus`. Values used by this endpoint are shown in the type above. |
| `noScoreReason` | string \| null | More specific no-score reason when applicable; otherwise omitted. |
| `reason` | string | Optional human-readable explanation of the scoring state. Do not branch on this text. |
| `inBaseline` | boolean | Whether the computed `(t, r)` pair belongs to the LMFDB baseline. |
| `baselineUnlocked` | `false` | Dry-run verification cannot establish a baseline improvement because it does not compute a scoring discriminant. |
| `baselineDiscAbs` | string | Baseline discriminant as an unsigned decimal string when one is available; otherwise omitted. |

## Example response

The labels and explanatory text below illustrate the post-event response shape. Use the values returned by the endpoint.

```json
{
  "ok": true,
  "data": {
    "results": [
      {
        "status": "accepted",
        "label": "24T1234",
        "t": 1234,
        "r": 0,
        "scoreable": false,
        "scoringStatus": "no_score",
        "scoringReason": "post_event_validation_only",
        "noScoreReason": "post_event_validation_only",
        "reason": "Post-event validation results never affect official scoring or ranking",
        "inBaseline": false,
        "baselineUnlocked": false
      },
      {
        "status": "rejected",
        "reason": "expected 25 coefficients, found 3"
      }
    ],
    "description": "Candidate batch 12."
  }
}
```

## Rate limit

This endpoint uses a dedicated per-account anti-abuse quota of 1,000 calls per fixed 24-hour window in place of the general write-class account quota. It does not consume the deadline team's persistent batch budget. A rejected request returns `Retry-After` and `X-RateLimit-*` values for the verification quota that caused the rejection; see [Rate limiting](../../../rate-limiting.md).

This endpoint does not accept an idempotency key. Retrying repeats the verification work and consumes another verification-quota unit, even when the polynomial lines are unchanged. Retry transient `503` responses only with bounded backoff.

## Errors

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | The JSON body, `Content-Type`, `polynomials` field type, `description` field type, or an unknown field is invalid. |
| `404` | `NOT_FOUND` | IGP24 does not exist or is not publicly visible. |
| `413` | `FILE_TOO_LARGE` | The joined polynomial text or normalized JSON request exceeds its published byte limit. |
| `422` | `POLYNOMIAL_INPUT_INVALID` | The polynomial array is empty or exceeds the published item limit. |
| `422` | `SUBMISSION_META_INVALID` | `description` exceeds its published character limit. |
| `429` | `IGP24_SUBMISSION_RATE_LIMIT_EXCEEDED` | The caller reached the verification-only anti-abuse quota. |
| `503` | `VERIFICATION_UNAVAILABLE` | At least one well-formed polynomial could not receive a verdict because no verification executor was available. Retry the whole request with backoff. |
| `503` | `IGP24_SERVICE_UNAVAILABLE` | The IGP24 service itself is temporarily unavailable. |

See [Errors](../../../errors.md) for shared authentication and scope errors.
