# API Reference

Base URL: `https://seo.northwestcar.group` (dev: `http://localhost:3000`).

- All requests/responses are JSON.
- Authentication: session cookie (`nws_session`) set by login/register.
- Every response carries `x-request-id`.
- Error envelope:

```json
{ "error": { "code": "VALIDATION", "message": "…", "details": {} }, "requestId": "…" }
```

Codes: `UNAUTHENTICATED` 401 · `FORBIDDEN`/permission 403 · not found 404 ·
`VALIDATION` 400 · `RATE_LIMITED` 429 · conflict 409 · `QUEUE_UNAVAILABLE` 503 ·
`INTERNAL` 500.

Pagination: `?page=1&pageSize=25` (max 100); responses include `page`,
`pageSize`, `total`.

## Auth

| Method & path | Body | Notes |
| --- | --- | --- |
| POST `/api/auth/register` | name, email, password, companyName, businessType?, country?, timeZone? | Creates user + organisation (OWNER), sends verification email, sets session. 201. Rate-limited. |
| POST `/api/auth/login` | email, password | Sets session. Lockout after 8 failures. Rate-limited. |
| POST `/api/auth/logout` | — | Destroys session. |
| POST `/api/auth/verify-email` | token | Single-use, 24 h TTL. |
| POST `/api/auth/request-password-reset` | email | Always 200 (no account enumeration). Rate-limited. |
| POST `/api/auth/reset-password` | token, password | Single-use, 30 min TTL; destroys all sessions. |

## Organisations

| Method & path | Permission | Notes |
| --- | --- | --- |
| GET `/api/organisations` | member | List caller's organisations with role + website count. |
| GET `/api/organisations/:orgId` | org.read | |
| PATCH `/api/organisations/:orgId` | org.update | name, businessType, country, timeZone, approvalMode. Audited. |

## Websites

| Method & path | Permission | Notes |
| --- | --- | --- |
| GET `/api/organisations/:orgId/websites` | website.read | Includes latest crawl summary. |
| POST `/api/organisations/:orgId/websites` | website.create | URL is SSRF-validated and canonicalised to origin; creates HTML_FILE / META_TAG / DNS_TXT verification tokens. 409 on duplicate. |
| GET `/api/websites/:websiteId` | website.read | Website + verifications + latest crawl + latest score. |
| PATCH `/api/websites/:websiteId` | website.update | name, platform, primaryService, targetAudience, crawlPageLimit (≤10 000), crawlDelayMs. |
| DELETE `/api/websites/:websiteId` | website.archive | Soft archive; history retained. |
| POST `/api/websites/:websiteId/verify` | website.update | Body `{ method: "HTML_FILE" \| "META_TAG" \| "DNS_TXT" }`; performs the live check and records the result. |

## Crawls

| Method & path | Permission | Notes |
| --- | --- | --- |
| GET `/api/websites/:websiteId/crawls` | website.read | Paginated crawl history. |
| POST `/api/websites/:websiteId/crawls` | crawl.start | 202 with crawl row; 409 if one is active; 503 if queue down (crawl marked FAILED, visible to user). |
| GET `/api/crawls/:crawlId` | audit.read | Progress counters + issue counts by severity. |
| POST `/api/crawls/:crawlId/cancel` | crawl.cancel | Requests cancellation; engine stops between pages. |
| GET `/api/crawls/:crawlId/issues` | audit.read | Paginated; filters `?severity=` `?type=`. |

## Platform

| Method & path | Notes |
| --- | --- |
| GET `/api/health` | `{ status, checks: { database, redis }, environment, version }` — 503 when degraded. No secrets. |

## Roadmap routes (not yet implemented — do not document as live)

Change password, 2FA enrol/disable, member invites/roles, integrations OAuth
(`/api/integrations/google/callback`, `/api/integrations/wordpress/callback`),
keywords, content, changes/approvals, reports, billing
(`/api/webhooks/stripe`). Tracked in TASKS.md.
