# Visits & PDF API

A read-only API for third-party integrations (e.g. a mobile app) to look up
a reviewer's saved visits by phone number, and to preview or download the
exact PDF menu plan a consultant would see or email.

A **visit** is a one-day menu plan saved from the reviewer form's meal
planner preview. Each visit has:

- a **preview** — the same content as the PDF, rendered as an HTML page
  (for an in-app WebView)
- a **PDF** — the actual file, byte-identical to what the consultant can
  download or email from the portal

## Base URL

```
https://<your-domain>/api/v1
```

## Authentication

There are two, deliberately different, ways requests are authorized:

| Endpoint | Auth |
|---|---|
| `GET /visits` | `X-Api-Key` header |
| `GET /visits/{id}/preview` | signed URL (no header) |
| `GET /visits/{id}/pdf` | signed URL (no header) |

**Why two mechanisms?** `GET /visits` is the only endpoint your backend
calls directly, so it's the only one that needs your API key. The
`preview_url`/`pdf_url` it returns are **pre-signed, expiring links** —
each one is only valid for one specific visit, for one specific phone
number, until it expires. That's what lets you hand a `pdf_url` straight
to a mobile WebView, browser tab, or another system, without that
component ever needing to know your API key.

Do not expose your API key to end users or client-side app code — treat it
like a password. Only your own backend should call `GET /visits`.

### Getting an API key

Ask whoever manages the Smart Carb server to run:

```
php artisan api-client:create "Your App Name"
```

This prints the key **once**. It looks like `sc_live_xxxxxxxxxxxxxxxxxxxx…`.
Only its hash is stored — if it's lost, a new one has to be issued
(`api-client:create` again); the old one can be turned off with:

```
php artisan api-client:revoke "Your App Name"
```

Send the key on every `GET /visits` call:

```
X-Api-Key: sc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## Rate limiting

All endpoints under `/api/v1` are limited to **30 requests/minute** per IP.
A request over that limit gets `429 Too Many Requests`.

---

## `GET /visits`

List a reviewer's visits by phone number.

### Request

```
GET /api/v1/visits?phone=966501234567
X-Api-Key: sc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

| Param | Type | Required | Notes |
|---|---|---|---|
| `phone` | string | yes | Any formatting is fine — digits and a leading `+` are kept, everything else (spaces, dashes, parentheses) is stripped before matching. |

### Response — `200 OK`

```json
{
  "status": true,
  "data": [
    {
      "id": 8,
      "date": "2026-09-13",
      "diet_type": "Weight Reduction - Phase 1",
      "target_calories": 2091,
      "status": "draft",
      "preview_url": "https://your-domain/api/v1/visits/8/preview?expires=1789720766&phone=966501234567&signature=…",
      "pdf_url": "https://your-domain/api/v1/visits/8/pdf?expires=1789720766&phone=966501234567&signature=…"
    }
  ],
  "links_expire_at": "2026-09-18T11:39:26+03:00"
}
```

Visits are newest-first. If the phone number matches no reviewer, or the
reviewer has no saved visits yet, `data` is an empty array (still `200`,
not an error).

`preview_url`/`pdf_url` are only valid until `links_expire_at` — after
that, call `GET /visits` again for fresh links. The window defaults to
**24 hours** and is configured server-side
(`SMARTCARB_API_LINK_TTL_MINUTES` in `.env`, default `1440`).

Don't try to construct `preview_url`/`pdf_url` yourself — the `signature`
query parameter is a server-side HMAC over the whole URL and can't be
reproduced without the server's app key. Always use the URLs exactly as
returned.

### Errors

| Status | When |
|---|---|
| `401` | Missing or invalid `X-Api-Key` |
| `422` | `phone` missing, or empty after stripping formatting |
| `429` | Rate limit exceeded |

---

## `GET /visits/{id}/preview`

Renders the visit as an HTML page — open this directly in a WebView or
browser tab. No `X-Api-Key` needed; the URL itself (from `GET /visits`)
is the credential.

### Request

```
GET https://your-domain/api/v1/visits/8/preview?expires=…&phone=966501234567&signature=…
```

Use the full URL exactly as returned by `GET /visits` — don't drop or
reorder the query parameters, or the signature check will fail.

### Response

`200 OK` with `Content-Type: text/html` — a complete, styled HTML page
(same layout as the PDF): reviewer name/file number, daily calorie/macro
targets, and every meal with its components, portions, and macros.

### Errors

| Status | When |
|---|---|
| `403` | Link expired, tampered with, or the `phone` no longer matches this visit's reviewer |
| `404` | No visit with that id |

---

## `GET /visits/{id}/pdf`

Streams the actual PDF file (`Content-Disposition: inline`, so a browser
or mobile PDF viewer opens it in place rather than forcing a download).
Same auth model as `preview` — use the `pdf_url` exactly as returned.

### Request

```
GET https://your-domain/api/v1/visits/8/pdf?expires=…&phone=966501234567&signature=…
```

### Response

`200 OK` with `Content-Type: application/pdf`.

### Errors

Same as `preview` above.

---

## Typical flow

1. Your backend calls `GET /visits?phone=…` with your `X-Api-Key`.
2. It stores/forwards the `preview_url`/`pdf_url` for whichever visit the
   app is about to show.
3. The mobile app opens `preview_url` in a WebView (in-app preview) and/or
   `pdf_url` in a PDF viewer/browser (download or native preview) —
   directly, no key needed, no round-trip through your backend.
4. If a link has expired (`links_expire_at` has passed), go back to step 1
   for a fresh one.

## Security notes

- The API key authenticates **your backend**, not an individual reviewer —
  never embed it in the mobile app binary or send it to the device.
- Every preview/pdf link is scoped to one visit **and** one phone number,
  and expires — even if a link leaks, it's only useful for that one visit,
  for a limited time, and only while that phone number still owns that
  visit.
- All of this rides over the same HTTPS your API key is sent over —
  there's no additional certificate/pinning requirement on your end.
