# Nivara Publish Protocol (v1)

How a site receives content from Nivara. The WordPress plugin (`wordpress-plugin/nivara`) implements it;
a custom CMS implements the same three endpoints. A runnable reference implementation is in
`examples/custom-cms-receiver.mjs`.

## 1. Connecting

1. In Nivara, the user adds the site with platform **custom** and gets a connect code (`NV-` + 8 hex characters).
2. The CMS calls Nivara (server to server):

   ```http
   POST {NIVARA_URL}/api/sites/connect
   Content-Type: application/json

   {
     "code": "NV-1A2B3C4D",
     "siteUrl": "https://example.com",          // host must equal the domain registered in Nivara
     "endpoint": "https://example.com/api/nivara", // base URL of the endpoints below; same domain or a subdomain
     "platform": "custom",
     "software": "my-cms", "version": "2.3", "siteName": "My site"
   }
   ```

   Response: `{ "ok": true, "siteId": "<uuid>", "secret": "<random>", "verifyUrl": "..." }`.
   Store `siteId` and `secret` securely. Errors return `{ "ok": false, "error": "<Persian message>" }` with HTTP 400/429.

3. Right after storing them, the CMS calls:

   ```http
   POST {NIVARA_URL}/api/sites/verify
   { "siteId": "<uuid>" }
   ```

   Nivara then calls `GET {endpoint}/status` with a signed request. The site is marked **connected** only if
   that call succeeds, which proves the endpoint belongs to the registered domain. Response: `{ "ok": true }`.

Connecting again (for example after losing the secret) issues a new secret; the old one stops working.

## 2. Request signing

Every request from Nivara to the site carries:

| Header | Value |
|---|---|
| `X-Nivara-Site` | the `siteId` |
| `X-Nivara-Timestamp` | Unix time in seconds |
| `X-Nivara-Signature` | `hex(HMAC-SHA256(secret, "{timestamp}.{METHOD}.{path}.{body}"))` |
| `X-Nivara-Protocol` | `1` |

`path` is the endpoint path relative to `endpoint` (`/status`, `/publish`, `/disconnect`); `body` is the raw
request body (empty string for GET). The site must:

* reject a mismatched `X-Nivara-Site`, a timestamp more than 300 seconds from its clock, or a wrong signature (compare in constant time);
* reject a signature it has already seen in the last 10 minutes (replay protection);
* answer with HTTP 401 and `{ "message": "..." }` when rejecting.

## 3. Endpoints

### `GET {endpoint}/status`

```json
{ "ok": true, "software": "my-cms", "version": "2.3", "siteName": "My site", "postCount": 128, "seo": null }
```

`postCount` (published posts) is optional; Nivara shows it in the panel.

### `POST {endpoint}/publish`

Creates the post the first time a `jobId` is seen and **updates the same post** on later calls (regenerated or
edited content, draft → publish). Must be idempotent.

```json
{
  "protocol": 1,
  "jobId": "uuid",
  "mode": "publish",                 // or "draft"
  "title": "...", "slug": "english-slug", "excerpt": "...",
  "html": "<p>...</p>",              // clean HTML body with FAQ section and FAQPage JSON-LD (no <h1>)
  "seo": { "metaTitle": "...", "metaDescription": "...", "focusKeyword": "...", "secondaryKeywords": ["..."] },
  "category": "...", "tags": ["..."],
  "featuredImage": { "url": "https://... or null", "alt": "..." },
  "faqSchema": { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [] },
  "options": { "createCategories": true },
  "document": { /* the full structured document: blocks, faq, internalLinkIdeas, ... */ }
}
```

Response:

```json
{ "ok": true, "postId": "123", "url": "https://example.com/blog/english-slug", "status": "publish", "postCount": 129 }
```

`postId` is required; `url` is shown to the user. On failure return a non-2xx status with `{ "message": "<reason>" }`;
Nivara records it and (for scheduled posts) retries with backoff, up to 5 times.

### `POST {endpoint}/disconnect`

Body `{}`. Forget `siteId` and `secret`. Sent when the user disconnects the site in Nivara.

## 4. Timing

* Unscheduled orders are sent right after generation: `publish` if the user enabled automatic publishing, otherwise `draft`.
* Scheduled orders are sent with `mode: "publish"` at their time (checked every few seconds).
* Users can also publish, send as draft, or update an already published post from the panel at any time.

## 5. Ordering API (CMS → Nivara)

A connected custom CMS can also order content, check balance, and top up the wallet through signed requests to `/api/v1` (same HMAC scheme; `path` is the part after `/api/v1`, including the query string). See `/platform/dev` (section "Ordering") or `NIVARA-CMS-API.txt` for the full reference.
