# Requests and errors

## Basics

- Base URL: `https://YOUR-MYFORGE/api/v1`.
- Send and receive JSON. Send `Accept: application/json` and, for bodies, `Content-Type: application/json`.
- Times are ISO 8601 (`2026-10-09T10:00:00+00:00`).

## Public ids

Servers, sites, deployments, backups, commands and similar records are identified by a public id (a UUID). You find it in the list endpoints and in the dashboard address. The internal numeric id is never used in the API. A wrong or out-of-reach id answers `404`.

## Lists and pages

List endpoints return a page of results:

```json
{
  "data": [ { "id": "...", "domain": "app.example.com" } ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "https://YOUR-MYFORGE/api/v1/sites?page=2" },
  "meta": { "current_page": 1, "per_page": 50, "total": 61 }
}
```

Follow `links.next` until it is `null`. Page sizes: sites and servers 50, deployments 25, backups 25, deletion requests 50. The audit log (`/audit-log`) returns 50 per page with `links.next` and `links.prev` and no total.

## Single records and errors

A single record comes as `{"data": {...}}`. Errors are JSON:

```json
{ "message": "No such site." }
```

For `422` (invalid input), `errors` names each field:

```json
{
  "message": "The domain field is required.",
  "errors": { "domain": ["The domain field is required."] }
}
```

| Status | Meaning |
| --- | --- |
| `401` | Token missing, wrong or expired. |
| `403` | A check failed: scope, plan, owner rule, permission, or IP. |
| `404` | Does not exist, belongs to another team, or is outside the token's limits. |
| `409` | Not possible right now: a deploy is already running, wrong site state, nothing to promote. |
| `422` | Invalid input, plan limit reached, or reused Idempotency-Key with a different request. |
| `429` | Too many requests. Wait and retry. |

## Idempotency-Key

Add an `Idempotency-Key` header (1 to 120 characters, unique per intended action) to every change (POST, PUT, PATCH, DELETE):

```bash
curl -X POST $MYFORGE/sites/SITE_ID/deploy \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: deploy-shop-2026-10-09-1"
```

How it behaves:

- The first answer is kept for 24 hours, per token.
- Sending the same key with the same request again returns the stored answer and the header `Idempotent-Replayed: true`. Nothing runs twice.
- The same key with a different method, path or body answers `422`.
- A retry while the first request is still running answers `409`. Wait a moment and retry.
- Answers that carry a secret shown only once (a WordPress admin password, a new database password, a new webhook secret) are never stored. Replaying them is not possible, so save the secret the first time.

Use a new key for each new action. For a retry after a timeout, use the same key.

## Rate limits

- 120 requests per minute per token.
- 30 changes per minute per token (changes are POST, PUT, PATCH, DELETE).

When you get `429`, wait (the `Retry-After` header tells how long, when present) and try again. Do not poll faster than every 5 to 10 seconds.

## The request log

Every call is logged: method, route, path, status, IP address and duration. Bodies are never logged. The log is kept for 30 days and is shown under **API**, then **Activity** (last 100 calls). Changes also appear in the team's audit log as source `api` with the token's name. See [Teams and the audit log](/developers/teams-and-audit-log).

## Slow jobs: 202 and polling

Most changes on servers take time, so the API answers `202 Accepted` (or `201` for new records) as soon as the job is queued. The work happens afterwards over SSH. To know when it is done, read the record again:

| You did | Poll | Done when |
| --- | --- | --- |
| Create a site | `GET /sites/{site}` | `status` is `active` (or `failed`) |
| Deploy, roll back | `GET /sites/{site}/deployments/{id}` | `status` is `succeeded` or `failed` |
| Run a command | `GET /sites/{site}/commands/{id}` | `status` is final; read `output` and `exit_code` |
| Back up | `GET /sites/{site}/backups/{id}` | `status` stops changing (an `error` field explains a failure) |
| Renew certificate | `GET /sites/{site}/certificate` | `expires_at` moved, history shows renewed |
| Add or connect a server | `GET /servers/{id}` | `status` is `active`, `adopted` or `failed` |
| Move a site | `GET /sites/{site}/move` | the move reaches its final state |

Polling pattern:

```bash
until [ "$(curl -s -H "Authorization: Bearer $TOKEN" $MYFORGE/sites/$SITE | jq -r .data.status)" != "creating" ]; do
  sleep 10
done
```

Stop after a sensible time (for example 15 minutes for a deploy) and report instead of looping forever.
