# Idea Italia S.A. -- Portal API guide (for LLMs and integrators)

> This document is machine-readable. It explains how to authenticate as a CLIENT
> and call the JSON API to generate a Cotizador Manual quotation and download its
> Excel. Everything here requires a valid client account; there is no anonymous
> pricing. Prices are returned as INTEGER CENTS (divide by 100 for USD).

Base URL: https://www.ideaitaliasa.com
Discovery: https://www.ideaitaliasa.com/llms.txt (site summary) and https://www.ideaitaliasa.com/manuales (human manual).

## What you can do
- Calculate a priced quotation from a list of cabinet codes (per-CAT prices + totals).
- Download that quotation as an .xlsx (Excel) file.
Quotes are scoped to the client's entitled bibliotecas, country, and discount --
all resolved SERVER-SIDE from the logged-in client (never from the request body).

Accounts are SELF-SERVICE: a client can register at https://www.ideaitaliasa.com/auth/register (no need to
"request" one). Pricing still requires a signed-in client account.

## Where the codes come from (Revit families -> Furniture schedule)
The `codes` you send are Idea Italia module codes. A client can produce the list from
Revit instead of typing it:
1. In the portal, open "Familias Revit", pick an entitled biblioteca, and download the
   `.rfa` families (streamed from `GET /portal/familias-revit/<bib>/<family>/descargar`;
   entitlement-scoped, rate-limited).
2. In Revit, place the families in a project and create a Schedule/Quantities of the
   "Furniture" category that lists each type's module code.
3. Paste that column of codes into `codes` (one per line) and CALCULATE below.
So an assistant can take a client's Revit "Furniture" schedule and drive the entire
quote through this API.

## Concepts (four distinct actions -- do not conflate them)
- CALCULATE (POST /api/internal/cotizador-manual): transient priced quote. Nothing
  is persisted. Use it to show prices.
- SNAPSHOT (POST /api/internal/cotizador-manual/snapshot): persists an IMMUTABLE,
  content-hashed VERIFICATION record and returns its hash + short code + a
  /portal/validar-cotizacion/<hash> validation URL + a QR image. This makes a quote
  VERIFIABLE **without** adding it to the client's saved list. Idempotent by content
  (identical content returns the same hash and does NOT consume the daily quota).
- EXCEL (POST /api/internal/cotizador-manual/excel): downloads the .xlsx AND creates
  the same verification snapshot (so the file's QR is verifiable). Also NOT saved to
  the list.
- SAVE (POST /api/internal/cotizador-manual/save): persists the quote to the client's
  "Cotizaciones guardadas" (named + re-openable) and OPTIONALLY links a verification
  snapshot hash.

Verifiability (SNAPSHOT / EXCEL) and saving-to-the-list (SAVE) are INDEPENDENT: a
quote can be verifiable without being saved, and saved without being verifiable (SAVE
can optionally link a snapshot hash). You never need to SAVE just to make a quote
verifiable.

CAT_n = a finish/price tier (e.g. CAT_1, CAT_2). Each priced code carries a price per
CAT; totals are per CAT. Money is INTEGER CENTS.

## Authentication (step by step)

The portal uses Firebase Auth for identity and a Flask session cookie for the API.
Session-changing POSTs are CSRF-protected. On HTTPS the CSRF check ALSO validates the
`Referer` header (so send a matching `Referer`). The examples also send `Origin` --
browsers add it automatically and it never hurts, though the server checks `Referer`.

### Credentials and safety (env-only; keep the password OUT of the model's prompt)
The sign-in below needs a CLIENT's Firebase email + password. Treat them as secrets:

- NEVER paste the email/password into a hosted AI chat (ChatGPT/Claude web, a shared
  window, a ticket). Anything typed there is retained in that provider's transcript
  and logs, and a leaked password grants FULL account access. If it ever lands in a
  chat, rotate it (reset the client's password via /auth/forgot).
- Intended use is a LOCAL agent/script on the user's OWN machine. Keep the credential
  in a gitignored, permission-restricted env file that ONLY local code reads at
  runtime; the value goes straight to the sign-in endpoint over HTTPS and never enters
  the model's context. The tool touches the secret; the conversation never does.

`creds.env` (gitignored, `chmod 600`, stays on the user's machine, never committed):
```
II_CLIENT_EMAIL=client@example.com
II_CLIENT_PASSWORD=your-password        # never commit; never paste into a chat
```

A small script the agent runs reads the ENVIRONMENT -- the agent sees the variable
NAME, not the value (so the plaintext never reaches the prompt/transcript):
```
python3 - <<'PY'
import os, requests
resp = requests.post(
    "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword",
    params={"key": "AIzaSyAnUDU2yx6VK3wZrX3MUX6kJbRqEfMzPKs"},
    json={
        "email": os.environ["II_CLIENT_EMAIL"],
        "password": os.environ["II_CLIENT_PASSWORD"],
        "returnSecureToken": True,
    },
    timeout=20,
)
resp.raise_for_status()
print(resp.json()["idToken"])           # feed into step 3 as <idToken-from-step-1>
PY
```
Shell equivalent (env loaded first; the JSON body is built from the env vars so the
password is never inlined): `set -a; . ./creds.env; set +a` then reference
`$II_CLIENT_EMAIL` / `$II_CLIENT_PASSWORD` in the curl `-d` body.

The `CLIENT_EMAIL` / `CLIENT_PASSWORD` placeholders in step 1 below are ONLY for a
human running it by hand; when an assistant is involved, use the env-file pattern above.

### 1. Sign in with Firebase (email + password) -> idToken
The Firebase Web API key is public client config:

```
curl -s "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyAnUDU2yx6VK3wZrX3MUX6kJbRqEfMzPKs" \
  -H "Content-Type: application/json" \
  -d '{"email":"CLIENT_EMAIL","password":"CLIENT_PASSWORD","returnSecureToken":true}'
# -> JSON; take the "idToken" field.
```

### 2. Get a CSRF token + session cookie
```
curl -s -c jar.txt "https://www.ideaitaliasa.com/auth/login" -o login.html
# CSRF token is in: <meta name="csrf-token" content="...">
```

### 3. Exchange the idToken for an authenticated session
Send the CSRF token in the `X-CSRFToken` header plus a matching `Referer` (the example
also sends `Origin`):

```
curl -s -c jar.txt -b jar.txt -X POST "https://www.ideaitaliasa.com/auth/session" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <csrf-from-step-2>" \
  -H "Referer: https://www.ideaitaliasa.com/auth/login" \
  -H "Origin: https://www.ideaitaliasa.com" \
  -d '{"id_token":"<idToken-from-step-1>"}'
# -> {"success": true, "redirect": "/portal/"}
```

### 4. For every later POST: fresh CSRF + the session cookie
The session rotates, so keep using the cookie jar (`-c jar.txt -b jar.txt`) and read
a FRESH CSRF token from any authenticated page before each POST:

```
curl -s -c jar.txt -b jar.txt "https://www.ideaitaliasa.com/portal/" -o portal.html
# CSRF is again in <meta name="csrf-token" content="...">
```

## Endpoints

### POST /api/internal/cotizador-manual  (calculate a quote)
Body: `{"codes": "CODE1\nCODE2\nCODE3,QTY"}`
- `codes`: newline-separated cabinet codes. A line may be `CODE` (qty 1) or
  `CODE,QTY`. Codes are matched case-sensitively within the client's entitled
  bibliotecas; unknown codes come back in `not_found`.

```
curl -s -c jar.txt -b jar.txt -X POST "https://www.ideaitaliasa.com/api/internal/cotizador-manual" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <fresh-csrf>" \
  -H "Referer: https://www.ideaitaliasa.com/portal/" -H "Origin: https://www.ideaitaliasa.com" \
  -d '{"codes":"GTGP90\nGTGP60D\nGTGBXL90\nGTGB60D"}'
```
Returns (200): `{"success": true, "company_name": "...", "rows": [...],
"cat_columns": [{"field":"CAT_1_price","label":"CAT_1"}, ...],
"totals": {"CAT_1_price": <integer-cents>, ...}, "not_found": []}`
(money in INTEGER CENTS; real amounts are returned only to the signed-in client -- this
is a shape example, not a price. `success:false` with `error` on a malformed/over-cap input).

### POST /api/internal/cotizador-manual/excel  (download the quote .xlsx)
Same body as above. Streams an `.xlsx` (Content-Type
`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) with the brand
letterhead, the priced matrix, and a verification code + validation link.

```
curl -s -c jar.txt -b jar.txt -X POST "https://www.ideaitaliasa.com/api/internal/cotizador-manual/excel" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <fresh-csrf>" \
  -H "Referer: https://www.ideaitaliasa.com/portal/" -H "Origin: https://www.ideaitaliasa.com" \
  -d '{"codes":"GTGP90\nGTGP60D\nGTGBXL90\nGTGB60D"}' \
  -o cotizacion.xlsx
```

### POST /api/internal/cotizador-manual/snapshot  (verifiable key/QR; NOT saved to the list)
Same body as calculate. Persists an immutable verification snapshot and returns the
key + validation URL + QR. Use this to make a quote VERIFIABLE without saving it to
"Cotizaciones guardadas".

```
curl -s -c jar.txt -b jar.txt -X POST "https://www.ideaitaliasa.com/api/internal/cotizador-manual/snapshot" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <fresh-csrf>" \
  -H "Referer: https://www.ideaitaliasa.com/portal/" -H "Origin: https://www.ideaitaliasa.com" \
  -d '{"codes":"GTGP90\nGTGP60D\nGTGBXL90\nGTGB60D"}'
```
Returns (200): `{"success": true, "hash": "<64-hex>", "short_hash": "94fb-a9f1-...",
"validation_url": "https://www.ideaitaliasa.com/portal/validar-cotizacion/<hash>", "valid_until_display":
"...", "qr_data_url": "data:image/png;base64,...", ...quote}`. Idempotent by content
(re-issuing the same quote returns the same hash and does not consume the daily quota).

### POST /api/internal/cotizador-manual/save  (save to "Cotizaciones guardadas")
Body: the quote (`codes` or `lines`) + `name` (REQUIRED) + optional `project`,
`client_ref`, `notes`, and optional `snapshot_hash` (link a snapshot you already
created). Persists a named, re-openable saved quote. A per-client cap applies.

```
curl -s -c jar.txt -b jar.txt -X POST "https://www.ideaitaliasa.com/api/internal/cotizador-manual/save" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <fresh-csrf>" \
  -H "Referer: https://www.ideaitaliasa.com/portal/" -H "Origin: https://www.ideaitaliasa.com" \
  -d '{"codes":"GTGP90\nGTGP60D\nGTGBXL90\nGTGB60D","name":"Cocina Perez","snapshot_hash":"<hash-from-snapshot>"}'
```
Returns (200): `{"success": true, "id": "<saved-quote-id>"}` (or `success:false` +
`error` on a blank/too-long name or the per-client cap).

## For assistants / LLMs: snapshot vs save (important)
- SHOW prices: CALCULATE. Nothing is persisted.
- Give the client a VERIFIABLE quote (a code + QR they can validate later) WITHOUT
  cluttering their saved list: SNAPSHOT (or EXCEL if they also want the file). You do
  NOT need to SAVE for verifiability.
- Add the quote to the client's "Cotizaciones guardadas": SAVE. This is a persistent,
  user-owned action -- ALWAYS confirm with the user first and pass a meaningful
  `name`. Do NOT save by default; the default posture is not-saved.

## Rules and limits
- Prices are INTEGER CENTS (USD). Divide by 100 for dollars.
- CSRF: `X-CSRFToken` header + a matching `Referer` on every POST (HTTPS enforces the
  `Referer`; the examples also send `Origin`, which is harmless). GET is CSRF-free.
- Entitlement: a client only prices codes in the bibliotecas assigned to their
  account. Country (Guatemala -> IVA) and discount come from the client record.
- Rate limits apply (HTTP 429 when exceeded); back off and retry.
- 401 with `error_code:"AUTH_REQUIRED"` (JSON/API requests) means the session or CSRF
  token is missing/invalid -- a missing or mismatched `Referer` on HTTPS also fails the
  CSRF check and lands here. Re-run steps 2-4.

## Where to go next
- Human manual (client login + portal usage): https://www.ideaitaliasa.com/manuales
- Site summary for LLMs: https://www.ideaitaliasa.com/llms.txt
- Contact: https://www.ideaitaliasa.com/contacto/
