# OpenAPI and clients

## The OpenAPI file

```text
GET https://YOUR-MYFORGE/api/v1/openapi.json
```

It is public (no token) and describes every endpoint with its scope (`x-scope`), request body, and response codes. In the dashboard it is also shown under **API**, then **API reference**. The pages in the API reference group of these docs are generated from it.

Generate a client with any OpenAPI 3.1 tool, for example `openapi-generator` or `openapi-typescript`. Set the base URL to `https://YOUR-MYFORGE/api/v1` and the security scheme to a bearer token. Add the `Idempotency-Key` header to every change.

For AI agents: `/llms.txt` and `/llms-full.txt`. See [AI with the REST API](/developers/ai-with-the-rest-api).

## curl

```bash
export MYFORGE=https://YOUR-MYFORGE/api/v1
export TOKEN=mf_YOUR_TOKEN

curl -s -H "Authorization: Bearer $TOKEN" $MYFORGE/sites

curl -s -X POST $MYFORGE/sites/SITE_ID/deploy \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: deploy-shop-20261009-1"
```

## JavaScript (Node 18+ or browser)

Never put a token in browser code that other people can open. Use this on a server or in CI.

```js
const base = 'https://YOUR-MYFORGE/api/v1';
const headers = { Authorization: `Bearer ${process.env.MYFORGE_TOKEN}`, Accept: 'application/json' };

const sites = await (await fetch(`${base}/sites`, { headers })).json();
console.log(sites.data.map((s) => s.domain));

const res = await fetch(`${base}/sites/${sites.data[0].id}/deploy`, {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
});
if (res.status !== 202) throw new Error(`Deploy refused: ${res.status} ${(await res.json()).message}`);
const deployment = (await res.json()).data;
```

## PHP

```php
$http = Http::withToken(env('MYFORGE_TOKEN'))
    ->acceptJson()
    ->baseUrl('https://YOUR-MYFORGE/api/v1');

$sites = $http->get('/sites')->throw()->json('data');

$response = $http
    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
    ->post("/sites/{$sites[0]['id']}/deploy");

if ($response->status() !== 202) {
    throw new RuntimeException($response->json('message'));
}
```

## Python

```python
import os, uuid, requests

base = "https://YOUR-MYFORGE/api/v1"
headers = {"Authorization": f"Bearer {os.environ['MYFORGE_TOKEN']}", "Accept": "application/json"}

sites = requests.get(f"{base}/sites", headers=headers, timeout=30).json()["data"]

r = requests.post(
    f"{base}/sites/{sites[0]['id']}/deploy",
    headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
    timeout=30,
)
r.raise_for_status()
deployment = r.json()["data"]
```

## Waiting for the deployment (Python)

```python
import time

while True:
    d = requests.get(f"{base}/sites/{site_id}/deployments/{deployment['id']}", headers=headers, timeout=30).json()["data"]
    if d["status"] in ("succeeded", "failed"):
        break
    time.sleep(10)
print(d["status"], d.get("code_findings"))
```

## Tips

- If you retry after a timeout, reuse the same `Idempotency-Key`. For a new action, make a new key.
- Follow `links.next` in lists.
- Handle `429` by waiting, and `409` by reading the state, not by retrying blindly.
- Set timeouts on every call.
