# API reference

> Every endpoint a Self-hosted team can call, with its request, response and errors.

Source: <https://frontieror-challenge.com/docs/self-hosted/api>

Run a whole Self-hosted session from a script. The [starter repository](https://frontieror-challenge.com/docs/self-hosted/example-code) uses these same endpoints, and that page has a complete client. To give this reference to an agent, use **Copy page** or the Markdown version from the menu next to the title.

## Basics

|                |                                                                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL       | `https://frontieror-challenge.com/api/v1`                                                                                              |
| Authentication | `Authorization: Bearer fo_…` with your personal API token. See [Authentication](https://frontieror-challenge.com/docs/authentication). |
| Format         | JSON with snake\_case fields. Times are ISO 8601 in UTC.                                                                               |
| Errors         | `{"error": "…"}`, with a message you can show as it is.                                                                                |
| Limits         | Request bodies up to 4 MB. Submissions count toward your team's daily limit: one per UTC day, 4 during the testing stage.              |

The examples use [httpx](https://www.python-httpx.org), which the starter uses too. Create the client once:

`client.py`

```python
import os

import httpx

api = httpx.Client(
    base_url="https://frontieror-challenge.com/api/v1",
    headers={"Authorization": f"Bearer {os.environ['FRONTIEROR_TOKEN']}"},
    timeout=60,
)
```

### Errors every endpoint can return

| Status | Message                                                         |
| ------ | --------------------------------------------------------------- |
| 401    | Missing or invalid API token.                                   |
| 403    | Join or create a team first. (Only endpoints that need a team.) |
| 500    | Something went wrong, try again.                                |

## Get your account

`GET /me` · Token

Who the token belongs to, their team, the current stage and round, and the submissions left today. Use it to check a token.

**Responses**

**200 OK** (application/json)

- `user` (object)
  - `id` (string)
  - `name` (string)
  - `email` (string)
- `team` (object · null): `null` until you join or create a team.
  - `id` (string)
  - `name` (string)
  - `track` (enum): `main` or `self-hosted`
- `stage` (object · null): The running stage, else the next one, else the last one.
  - `id` (string)
  - `name` (string)
  - `status` (enum): `upcoming`, `active` or `ended`.
  - `start_date` (string · date-time)
  - `end_date` (string · date-time)
- `round` (object · null): The current round of the stage: the open one, else the next one, else the last one. `null` if the stage has no rounds.
  - `number` (integer): 1-based.
  - `of` (integer): How many rounds the stage has.
  - `status` (enum): `upcoming`, `open` or `closed`. You can submit only while it is `open`.
  - `start_date` (string · date-time)
  - `end_date` (string · date-time)
  - `self_hosted_window_s` (integer · null): How long a Self-hosted submission in this round stays open, in seconds. See [Submission flow](https://frontieror-challenge.com/docs/self-hosted/submission-flow#1-open-a-submission). `null` until the round opens.
- `submissions_left_today` (integer · null): Resets at 00:00 UTC. `null` without a team or a stage.

**Python example**

```python
me = api.get("/me").json()
print(me["team"], me["submissions_left_today"])
```

**Example response: 200 OK**

```json
{
  "user": { "id": "5b0f…", "name": "Ada Lovelace", "email": "ada@example.com" },
  "team": { "id": "c2a1…", "name": "Column Generators", "track": "main" },
  "stage": {
    "id": "91de…",
    "name": "Stage 1",
    "status": "active",
    "start_date": "2027-01-04T05:00:00.000Z",
    "end_date": "2027-02-01T05:00:00.000Z"
  },
  "round": {
    "number": 2,
    "of": 4,
    "status": "open",
    "start_date": "2027-01-11T05:00:00.000Z",
    "end_date": "2027-01-18T05:00:00.000Z",
    "self_hosted_window_s": 5100
  },
  "submissions_left_today": 3
}
```

**Example response: 401 Unauthorized**

```json
{ "error": "Missing or invalid API token." }
```

## List problems

`GET /problems` · Token

The problems of the current round, once it has opened: the ones a submission covers. Their slugs are the names your solvers go by.

**Query parameters**

- `details` (flag, optional): Add `?details=1` to include each problem's description, schemas and checker, as in [Get a problem](#get-a-problem).

**Responses**

**200 OK** (application/json)

An array of problems, empty before the round opens.

- `slug` (string)
- `name` (string)
- `category` (enum): The leaderboard category: `routing`, `production_inventory`, `scheduling`, `packing_partitioning`, `graph_quadratic` or
  `network_design_location`
- `formulation_type` (enum): The leaderboard formulation: `combinatorial`, `mixed_integer`, `quadratic`, `stochastic` or `robust_bilevel`
- `objective` (enum): `minimize` or `maximize`

**Python example**

```python
problems = api.get("/problems").json()
slugs = [p["slug"] for p in problems]
```

**Example response: 200 OK**

```json
[
  {
    "slug": "barnhart2000",
    "name": "Origin-Destination Integer Multicommodity Flow (ODIMCF)",
    "category": "network_design_location",
    "formulation_type": "combinatorial",
    "objective": "minimize"
  }
]
```

## Get a problem

`GET /problems/{slug}` · Token

One problem of the current stage with its statement and schemas: the content of its problem page.

**Path parameters**

- `slug` (string, required)

**Responses**

**200 OK** (application/json)

- `slug` (string)
- `name` (string)
- `category` (enum): The leaderboard category: `routing`, `production_inventory`, `scheduling`, `packing_partitioning`, `graph_quadratic` or
  `network_design_location`
- `formulation_type` (enum): The leaderboard formulation: `combinatorial`, `mixed_integer`, `quadratic`, `stochastic` or `robust_bilevel`
- `objective` (enum): `minimize` or `maximize`
- `description` (string): The statement in Markdown, with TeX math.
- `instance_schema` (object): An annotated example of an instance file. See [Reading the schemas](https://frontieror-challenge.com/docs/problems#reading-the-schemas).
- `solution_schema` (object): An annotated example of a solution file.
- `solution_json_schema` (object): The solution schema as a [JSON Schema](https://json-schema.org), to validate solutions with any JSON Schema library. It
  checks the format only; feasibility is up to the checker.
- `checker` (string · null): The source of the checker our evaluation runs, `feasibility_check.py`. `null` when the problem's checker is not
  published. See [Checkers](https://frontieror-challenge.com/docs/problems#checkers).

**404 Not found**

No problem with this slug in the current stage.

**Python example**

```python
problem = api.get("/problems/barnhart2000").json()
print(problem["solution_schema"])
```

**Example response: 200 OK**

```json
{
  "slug": "barnhart2000",
  "name": "Origin-Destination Integer Multicommodity Flow (ODIMCF)",
  "category": "network_design_location",
  "formulation_type": "combinatorial",
  "objective": "minimize",
  "description": "…",
  "instance_schema": { "network": { … }, "commodities": { … } },
  "solution_schema": {
    "objective_value": "<float> Total cost of rejected commodities across the entire routing plan.",
    "commodities": [ … ]
  },
  "checker": "…"
}
```

**Example response: 404 Not found**

```json
{ "error": "No problem with this slug in the current stage." }
```

## List public instances

`GET /problems/{slug}/instances` · Token

The problem's public instances, for development. Private instances are never listed.

**Path parameters**

- `slug` (string, required)

**Responses**

**200 OK** (application/json)

An array of instances, ordered by name.

- `id` (string · uuid)
- `name` (string)
- `time_limit_s` (integer): Seconds a solver gets for this instance.

**404 Not found**

No problem with this slug in the current stage.

**Python example**

```python
instances = api.get("/problems/barnhart2000/instances").json()
```

**Example response: 200 OK**

```json
[
  { "id": "0f1e8c2a-…", "name": "tiny_instance", "time_limit_s": 60 }
]
```

## Download a public instance

`GET /problems/{slug}/instances/{id}` · Token

The instance file itself, in the problem's instance schema, sent as an attachment named `<slug>-<name>.json`.

**Path parameters**

- `slug` (string, required)
- `id` (string · uuid, required): From [List public instances](#list-public-instances).

**Responses**

**200 OK** (application/json)

The instance file.

**404 Not found**

This problem has no public instance with this id.

**Python example**

```python
from pathlib import Path

for instance in instances:
    data = api.get(f"/problems/barnhart2000/instances/{instance['id']}").content
    Path(f"{instance['name']}.json").write_bytes(data)
```

## Open a submission

`POST /self-hosted/submissions` · Token · Self-hosted team

Opens a submission and starts its window, which is sized to the round's instances. See [Submission flow](https://frontieror-challenge.com/docs/self-hosted/submission-flow#1-open-a-submission). It uses one of the day's submissions. There is no request body.

**Responses**

**201 Created** (application/json)

- `id` (string · uuid)
- `created_at` (string · date-time)
- `expires_at` (string · date-time): When the window closes.
- `instances` (object\[]): Every instance of the session, ordered by problem, then name.
  - `id` (string · uuid)
  - `name` (string)
  - `problem_slug` (string)
  - `time_limit_s` (integer)
  - `started_at` (string · date-time · null): When you started it.
  - `upload_deadline` (string · date-time · null): Its time limit plus 60 seconds after the start.
  - `submitted_at` (string · date-time · null): When its solution was uploaded.

**403 Forbidden**

You have no team, or your team is on the Main track.

**409 Conflict**

No round is open, the round has no Self-hosted instances yet, or your team already has an open submission: “Your team already has an open submission. Finish it before starting another.”

**429 Too many requests**

Your team has used today's submissions: one per UTC day, 4 during the testing stage.

**Python example**

```python
submission = api.post("/self-hosted/submissions").json()
base = f"/self-hosted/submissions/{submission['id']}"
```

**Example response: 201 Created**

```json
{
  "id": "7a1c0e52-…",
  "created_at": "2027-01-13T09:00:00.000Z",
  "expires_at": "2027-01-13T10:00:00.000Z",
  "instances": [
    {
      "id": "c4d9…",
      "name": "large_instance_2",
      "problem_slug": "barnhart2000",
      "time_limit_s": 60,
      "started_at": null,
      "upload_deadline": null,
      "submitted_at": null
    }
  ]
}
```

## Get the open submission

`GET /self-hosted/submissions/current` · Token · Self-hosted team

Your team's open submission, to resume a session. Same fields as [Open a submission](#open-a-submission), with the progress of each instance.

**Responses**

**200 OK** (application/json)

The open submission.

**404 Not found**

Your team has no open submission.

**Python example**

```python
r = api.get("/self-hosted/submissions/current")
submission = r.json() if r.status_code == 200 else None
```

## Start an instance

`POST /self-hosted/submissions/{id}/instances/{instanceId}` · Token · Self-hosted team

Starts the instance's clock on the first call and returns the instance file. Later calls, and `GET` on the same path, download it again until its deadline.

**Path parameters**

- `id` (string · uuid, required): The submission.
- `instanceId` (string · uuid, required): From the submission's `instances`.

**Responses**

**200 OK** (application/json)

The instance file, sent as an attachment named `<slug>-<name>.json`.

- `Upload-Deadline` (header · date-time): When the solution must be uploaded by: the start plus the time limit plus 60 seconds.

**404 Not found**

The submission or the instance does not exist, or the instance is not part of the submission.

**409 Conflict**

- This submission has closed.
- The time for this instance is over.
- Start this instance first. (A `GET` before the first `POST`.)
- Not enough time left: the submission closes before this instance's time limit would run out.

**Python example**

```python
from datetime import datetime

r = api.post(f"{base}/instances/{instance_id}")
instance = r.json()
deadline = datetime.fromisoformat(r.headers["Upload-Deadline"])
```

## Upload a solution

`PUT /self-hosted/submissions/{id}/instances/{instanceId}/solution` · Token · Self-hosted team

Uploads the instance's solution, once, before its deadline. The body is the solution file itself, at most 4 MB. See [Submission format](https://frontieror-challenge.com/docs/self-hosted/submission-format).

**Path parameters**

- `id` (string · uuid, required): The submission.
- `instanceId` (string · uuid, required)

**Responses**

**200 OK** (application/json)

- `submission_closed` (boolean): `true` when this was the last instance: the submission is closed and queued for evaluation.

**400 Bad request**

The request body is empty.

**404 Not found**

The submission or the instance does not exist.

**409 Conflict**

- The file is not valid JSON.
- The solution must be a JSON object.
- The solution has no numeric objective\_value.
- This submission has closed.
- Start this instance before uploading a solution.
- A solution for this instance has already been uploaded.
- The upload deadline for this instance passed at *time* UTC.

**413 Payload too large**

The body is larger than 4 MB.

**Python example**

```python
r = api.put(f"{base}/instances/{instance_id}/solution", json=solution)
if r.is_error:
    print(r.json()["error"])
elif r.json()["submission_closed"]:
    print("All instances uploaded")
```

**Example response: 200 OK**

```json
{ "submission_closed": false }
```

**Example response: 409 Conflict**

```json
{ "error": "A solution for this instance has already been uploaded." }
```

## Finish a submission

`POST /self-hosted/submissions/{id}/finish` · Token · Self-hosted team

Closes the submission before its window runs out. Instances without a solution score 0.

**Responses**

**200 OK** (application/json)

- `queued` (boolean): `true` when it was queued for evaluation; `false` when nothing was uploaded, so it is completed with a score of 0.

**404 Not found**

The submission does not exist.

**409 Conflict**

This submission has already closed.

**Python example**

```python
api.post(f"{base}/finish").json()
```

## Get a submission

`GET /submissions/{id}` · Token · team member

One of your team's submissions with its status and results. Poll it until `status` is `completed` or `failed`.

**Path parameters**

- `id` (string · uuid, required)

**Responses**

**200 OK** (application/json)

- `id` (string · uuid)
- `track` (enum): `self-hosted`
- `status` (enum): `started` (open), `pending` (queued), `running`, `completed` or `failed`.
- `score` (number · null): The submission's score once `completed`.
- `error` (string · null): Why a `failed` submission could not be evaluated.
- `created_at` (string · date-time)
- `updated_at` (string · date-time)
- `environment` (null): Only for the Main track.
- `problems` (object\[])
  - `slug` (string)
  - `name` (string)
  - `score` (number · null): The problem's score.
  - `instances` (object\[])
    - `name` (string)
    - `set` (enum): `private`
    - `score` (number · null)
    - `error` (string · null): Why the instance scored 0, such as “No solution submitted”. `null` for a feasible solution.

**404 Not found**

Your team has no submission with this id.

**Python example**

```python
import time

while True:
    s = api.get(f"/submissions/{submission['id']}").json()
    if s["status"] in ("completed", "failed"):
        break
    time.sleep(10)
print(s["score"])
```
