# Integration API

Server-to-server API exposed by **eleva-speed** for the vitrine (or any other
trusted Eleva-owned site) to trigger anonymous audits and fetch results.

This API is **not** for public/browser callers. It is meant to be called from
another server (Next.js Server Action, Node backend, etc.) that holds the
shared `ELEVASPEED_INTEGRATION_API_KEY`.

---

## Authentication

Every request must include:

```
Authorization: Bearer <ELEVASPEED_INTEGRATION_API_KEY>
```

The key is shared between the vitrine and eleva-speed via environment
variables (`ELEVASPEED_INTEGRATION_API_KEY`). Generate one with:

```bash
openssl rand -base64 48
```

Minimum 32 characters. The server compares with `timingSafeEqual` to avoid
timing attacks.

If the key is not configured server-side, every request returns `503`.

---

## Base URL

```
https://<eleva-speed-host>/api/integrations
```

Replace `<eleva-speed-host>` with the production host (or `http://localhost:3000`
in dev).

---

## Rate limiting

- **10 requests / minute / client IP** on the audit endpoints.
- Exceeded → `429 { error: "rate_limited", message: "Retry in <n>s" }` with
  `Retry-After` header.

The "client IP" is read from `x-forwarded-for` then `x-real-ip`. If your vitrine
calls the API directly (server-to-server), the IP will be the vitrine's IP and
the limit applies to the whole vitrine. Consider forwarding the **end-user IP**
in `x-forwarded-for` if you want per-visitor limiting.

---

## `POST /audit` — start an anonymous audit

### Request

```http
POST /api/integrations/audit
Authorization: Bearer <ELEVASPEED_INTEGRATION_API_KEY>
Content-Type: application/json

{
  "url": "example.com",
  "device": "desktop"
}
```

- `url` — required. Hostname or full URL. Bare hostnames are auto-prefixed with `https://`.
- `device` — optional, `"desktop"` (default) or `"mobile"`.

### Success — `202 Accepted`

```json
{
  "id": "<test-id-from-sitespeed>",
  "status": "queued",
  "hostname": "example.com"
}
```

The audit is now running asynchronously. Poll `GET /audit/{id}` to follow
progress.

### Error — `403 auth_required` (second audit on same hostname)

If an anonymous audit was already started for this hostname (per the
[lead capture rules](../memory/eleva-speed-lead-capture-rules.md)), the second
attempt is blocked:

```json
{
  "error": "auth_required",
  "message": "Site example.com a déjà été audité anonymement. Créez un compte pour relancer un audit.",
  "signupUrl": "https://eleva-speed.app/signup",
  "existingAuditId": "<previous-id>",
  "existingAuditStatus": "completed"
}
```

The vitrine should:

1. Show the user the link to `signupUrl` (or open it in a popup).
2. After signup completes, the user lives in eleva-speed's authenticated
   dashboard, where unlimited audits are available. The vitrine flow ends here.

### Other errors

| Status | Body                                            | When                                                  |
| ------ | ----------------------------------------------- | ----------------------------------------------------- |
| `400`  | `{ error: "invalid_body", details: [...] }`     | Body fails Zod validation.                            |
| `400`  | `{ error: "invalid_url" }`                      | `url` can't be parsed.                                |
| `401`  | `{ error: "Missing/Invalid Authorization …" }`  | Missing or bad bearer.                                |
| `429`  | `{ error: "rate_limited", message: "Retry …" }` | Rate limit exceeded.                                  |
| `4xx`  | `{ error: "sitespeed_error", message: "…" }`    | Upstream sitespeed API returned an error.             |
| `503`  | `{ error: "Integration API not configured" }`   | `ELEVASPEED_INTEGRATION_API_KEY` missing server-side. |

---

## `GET /audit/{id}` — poll status + fetch result

Returns the full sitespeed result. Same `Authorization` header. **Only audits
started anonymously via this API are visible** (user-scoped audits remain
private to `/api/sites/*`).

### Request

```http
GET /api/integrations/audit/<id>
Authorization: Bearer <ELEVASPEED_INTEGRATION_API_KEY>
```

### Success — `200 OK`

```json
{
  "id": "...",
  "url": "https://example.com",
  "browser": "chrome",
  "device": "desktop",
  "iterations": 1,
  "status": "completed",
  "createdAt": "2026-05-23T14:00:00.000Z",
  "completedAt": "2026-05-23T14:00:42.000Z",
  "error": null,
  "metrics": {
    "firstContentfulPaint": 1234,
    "largestContentfulPaint": 2345,
    "totalBlockingTime": 30,
    "cumulativeLayoutShift": 0.05,
    "timeToFirstByte": 200,
    "firstInputDelay": 10,
    "speedIndex": 1800,
    "fullyLoaded": 3500,
    "domContentLoaded": 1100,
    "domInteractive": 1500,
    "requests": 42,
    "transferSize": 1200000,
    "resourceBreakdown": { "...": { "count": 0, "transferSize": 0, "time": 0 } }
  },
  "lighthouse": {
    "scores": { "performance": 92, "accessibility": 98, "best-practices": 100, "seo": 100 },
    "opportunities": [],
    "diagnostics": [],
    "reportUrl": "/lighthouse/..."
  },
  "har": "/har/...",
  "reportUrl": "/report/...",
  "screenshots": {
    "afterPageCompleteCheck": "/screenshots/...",
    "largestContentfulPaint": "/screenshots/...",
    "layoutShift": "/screenshots/..."
  }
}
```

`status` cycles through `queued` → `running` → `completed` (or `failed`).
Poll every ~2 seconds; expect the typical audit to take 30-90s.

### Errors

| Status | When                                                            |
| ------ | --------------------------------------------------------------- |
| `404`  | Audit ID doesn't exist locally, or sitespeed has lost the test. |
| `401`  | Missing or bad bearer.                                          |
| `503`  | Integration API not configured.                                 |

---

## curl examples

```bash
# Start
curl -X POST https://eleva-speed.app/api/integrations/audit \
  -H "Authorization: Bearer $ELEVASPEED_INTEGRATION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "example.com", "device": "mobile" }'

# Poll
curl https://eleva-speed.app/api/integrations/audit/<id> \
  -H "Authorization: Bearer $ELEVASPEED_INTEGRATION_API_KEY"
```

---

## Vitrine integration sample (Next.js Server Action)

```ts
// app/audit/actions.ts on the vitrine
"use server";

const API_BASE = process.env.ELEVA_API_BASE!; // e.g. https://eleva-speed.app
const API_KEY = process.env.ELEVASPEED_INTEGRATION_API_KEY!;

export async function startAudit(url: string, device: "desktop" | "mobile" = "desktop") {
  const res = await fetch(`${API_BASE}/api/integrations/audit`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url, device }),
    // Forward end-user IP so per-visitor rate-limiting works.
    // headers.set("x-forwarded-for", request.headers.get("x-forwarded-for") ?? "")
    cache: "no-store",
  });
  return { status: res.status, body: await res.json() };
}

export async function pollAudit(id: string) {
  const res = await fetch(`${API_BASE}/api/integrations/audit/${id}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
    cache: "no-store",
  });
  return { status: res.status, body: await res.json() };
}
```

When the vitrine receives `403 auth_required`, render a CTA pointing to
`body.signupUrl`. After signup the user lives on eleva-speed.

---

## Security notes

- The key MUST stay server-side. Never embed it in client-side JS.
- HTTPS is required in production — bearer tokens over HTTP are interceptable.
- This API exposes only anonymous audits. User-scoped data is unreachable here.
- The integration key is a single shared secret. For finer-grained access
  (per-vitrine quotas, revocation), upgrade to per-vitrine keys later.
