# Dokicasa API (Canone Concordato) — Istruzioni di integrazione per Claude Code

Questo file e' progettato per essere consegnato a **Claude Code** (o altri
assistenti AI) come prompt completo di integrazione delle **API server-to-server**
di Dokicasa per il flusso "Canone Concordato" (e analoghi).

> **Per Claude/assistente AI:** leggi tutto questo file prima di scrivere
> codice. Le sezioni "Vincoli", "Sicurezza" e "Common pitfalls" contengono
> regole non negoziabili. Se manca un parametro (token API, slug del form,
> external_id), CHIEDI all'utente prima di inventarlo. Non eseguire MAI
> chiamate `POST` o `PUT` senza aver prima validato con l'utente che il
> payload sia corretto.

---

## TL;DR

Queste sono **API REST server-to-server**: il tuo backend (PHP, Node, Python,
Go) parla con `https://app.dokicasa.it/api/...` usando un **Bearer token**.
Tutte le chiamate sono autenticate. Non c'e' frontend: per integrare un form
nel browser usa invece il [Web Component SDK](https://sdk.dokicasa.it/CLAUDE.md).

Endpoint principali:

| Method | Path | Cosa fa |
|---|---|---|
| `GET` | `/api/list-form/canone-concordato-{citta}` | Lista degli step del flusso per una citta |
| `GET` | `/api/v3/form/{slug}` | Schema (campi) di un form specifico |
| `POST` | `/api/v3/form/{slug}` | Submit form completo, crea una pratica |
| `PUT` | `/api/v3/form/{bundleUserServiceId}` | Update form gia' inviato |
| `GET` | `/api/v3/user/{id}/services` | Lista pratiche di un utente (filtrabile per external_id) |
| `GET` | `/api/v3/practices/{practice}` | Dettaglio + stato di una singola pratica (id+Bearer o uuid pubblico) |
| `GET` | `/api/v3/practices/{practice}/tasks` | Task collegati a una pratica (Activity type=TASK) |

Documentazione interattiva: <https://api.dokicasa.it/api-doc/>
Forms Explorer (playground): <https://api.dokicasa.it/api-doc/form.html>

---

## 1. Vincoli (non negoziabili)

- Il **Bearer token** NON deve mai finire in un file frontend, in un
  repository pubblico o in un log non sanificato.
- Tutte le chiamate vanno su **HTTPS**. Mai `http://` in produzione.
- L'`external_id` deve essere stabile e univoco per ogni end-user del partner:
  e' la chiave con cui ritroverai le sue pratiche dopo. Una volta scelto,
  non cambiarlo (consigliato: `uuid` o id interno del DB partner).
- L'endpoint `POST /api/v3/form/{slug}` puo' scalare credito dal wallet del
  partner. Verifica prima di chiamarlo in produzione che il wallet sia
  capiente, sennò ottieni `401`.
- Il webhook `webhook_url` configurato sul partner riceve eventi sia per i
  contratti creati via SDK frontend sia per quelli creati via API: gestisci
  l'idempotenza per evitare doppio processing.

---

## 2. Auth

Tutte le chiamate richiedono l'header:

```http
Authorization: Bearer <PARTNER_API_TOKEN>
```

Il token e' rilasciato da Dokicasa al partner master. E' a vita lunga
(non scade automaticamente); puoi rigenerarlo dal pannello partner.
La rigenerazione invalida il vecchio token dopo 24 ore.

Per ottenere/rigenerare il token: <integrations@dokicasa.it>.

---

## 3. Quick start — scenario tipico (PHP)

### Step 1 — Lista step di un flusso

```php
$res = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->get('https://app.dokicasa.it/api/list-form/canone-concordato-milano');

// $res->json() = [
//   ['step' => 1, 'nome' => 'Dati Immobile', 'slug' => '/api/v3/form/foglio-...'],
//   ['step' => 2, 'nome' => 'Calcolo Canone', 'slug' => '/api/v3/form/calcolo-...'],
//   ...
// ]
```

### Step 2 — Schema del primo form

```php
$schema = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->get('https://app.dokicasa.it/api/v3/form/foglio-caratteristiche-immobile-milano')
    ->json();

// $schema['form'] = { slug => { question, type, field_subtype, options, ... } }
```

### Step 3 — Submit del form

```php
$res = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->post('https://app.dokicasa.it/api/v3/form/foglio-caratteristiche-immobile-milano', [
        'form' => [
            'tipologia_immobile' => [
                'question' => '...',
                'type'     => 'selectable',
                'value'    => 'Appartamento',
            ],
            // tutti gli altri campi del form, con il loro `value` valorizzato
        ],
        'metadata' => [
            'external_id' => 'user-12345',     // OBBLIGATORIO
            'source'      => 'partner_landing',
        ],
    ])->json();

// $res = { id, bundle_user_service_id, external_id }
// SALVA bundle_user_service_id: ti serve per fare update con PUT.
```

### Step 4 — Update di una pratica gia' inviata

```php
Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->put('https://app.dokicasa.it/api/v3/form/' . $bundleUserServiceId, [
        'form' => [
            'tipologia_immobile' => ['value' => 'Villa'],   // basta il `value` da aggiornare
        ],
    ]);
```

### Step 5 — Ritrovare le pratiche dell'utente

```php
$practices = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->get('https://app.dokicasa.it/api/v3/user/me/services', [
        'filter[external_id]' => 'user-12345',
    ])
    ->json();

// $practices['data'] = array di PracticeSummary
```

### Step 6 — Stato di una pratica e relativi task

```php
$practiceId = 84213; // id pratica (oppure l'uuid pubblico)

// Dettaglio + stato corrente della pratica
$practice = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->get("https://app.dokicasa.it/api/v3/practices/{$practiceId}")
    ->json();

// echo $practice['status'];  // es. WAITING / DOING / ...

// Task collegati alla pratica (to-do list / avanzamento)
$tasks = Http::withToken(env('DOKICASA_TOKEN'))
    ->acceptJson()
    ->get("https://app.dokicasa.it/api/v3/practices/{$practiceId}/tasks")
    ->json();

// $tasks['tasks'] = array di Activity (type=TASK) con status TO_DO / DOING / DONE
```

> Con l'**uuid** al posto dell'id numerico le due GET sono pubbliche (senza Bearer):
> comodo per condividere un link in sola lettura. Con l'id numerico servono Bearer
> e proprieta' della pratica.

---

## 4. Quick start (Node.js)

```js
import fetch from 'node-fetch';

const TOKEN = process.env.DOKICASA_TOKEN;
const BASE  = 'https://app.dokicasa.it/api';
const auth  = { Authorization: `Bearer ${TOKEN}`, Accept: 'application/json' };

// 1. Lista step
const steps = await fetch(`${BASE}/list-form/canone-concordato-milano`, { headers: auth })
    .then(r => r.json());

// 2. Submit del primo form
const created = await fetch(`${BASE}/v3/form/${steps[0].slug.split('/').pop()}`, {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({
        form: { /* ... */ },
        metadata: { external_id: 'user-12345' },
    }),
}).then(r => r.json());

// 3. List per external_id
const params = new URLSearchParams({ 'filter[external_id]': 'user-12345' });
const list = await fetch(`${BASE}/v3/user/me/services?${params}`, { headers: auth })
    .then(r => r.json());
```

---

## 5. Lista pratiche di un utente (filtri completi)

`GET /api/v3/user/{id}/services` accetta:

| Parametro | Tipo | Note |
|---|---|---|
| `{id}` (path) | int o `me` | id utente; `me` = utente del token |
| `filter[external_id]` | string | LIKE match su Practice.external_id |
| `filter[name]` | string | full-text-ish (immobile, locatore, conduttore, ...) |
| `filter[from]` / `filter[to]` | date (`YYYY-MM-DD`) | range su `created_at` |
| `filter[types]` | enum | `PRATICHE` (UB/US/UMC) o `SIGNATURES` |
| `filter[signature_type]` | string | restringe solo a UserDigitalSignature |
| `status` | enum | `ALL` (default), `WAITING`, `DOING`, `LEAD`, `ANTIRICICLAGGIO`, `STEP_1_UNCOMPLETED`, `STEP_2_UNCOMPLETED` |
| `only_service_name` | string | filtra per nome del Service collegato |
| `page` | int | paginazione Laravel (default 1, 20 per pagina) |

Esempio composto:

```
GET /api/v3/user/me/services
    ?filter[external_id]=usr_9483dks
    &filter[from]=2026-01-01
    &filter[to]=2026-12-31
    &status=DOING
    &page=2
```

Response: paginatore Laravel standard
(`{ data: [...], current_page, per_page, total, last_page }`).

---

## 6. Webhook entranti (`practice.*`)

Configura `webhook_url` nel pannello partner. Dokicasa ti POSTa eventi
durante il ciclo di vita della pratica:

```json
{
  "event_type":           "PRACTICE_CREATED",
  "practice_id":          8930,
  "bundle_user_service_id": 456,
  "external_id":          "user-12345",
  "slug":                 "foglio-caratteristiche-immobile-milano",
  "timestamp":            "2026-05-22T14:32:00Z"
}
```

Event types che riceverai:
- `PRACTICE_CREATED` — appena crei via POST
- `PRACTICE_UPDATED` — dopo ogni PUT
- `PRACTICE_COMPLETED` — quando il flusso e' finito (PDF generato + firma)
- `PRACTICE_CANCELED`

### Idempotenza

Il body include un `request_id` univoco. Salvalo in DB e rifiuta i duplicati
con `200 OK` (NON con `409`, sennò Dokicasa rietnta).

### Verifica firma (TBD)

In v1 il webhook non e' firmato. Dalla v1.2 useremo HMAC-SHA256 con un
`webhook_secret` per partner. Per ora puoi proteggerti whitelistando gli IP
di Dokicasa nel tuo firewall: chiedi la lista a integrations@dokicasa.it.

---

## 7. Common pitfalls

- **CORS:** non si applica (server-to-server, non browser). Se vedi errori
  CORS, hai sbagliato e stai chiamando dal client invece che dal server.
- **`401 Unauthorized` sul POST:** wallet partner esaurito. Verifica il
  saldo dal pannello prima di chiamare.
- **`404` sulla submit:** lo `slug` del form non esiste per quella citta.
  Controlla con `GET /api/list-form/canone-concordato-{citta}`.
- **`422` Form field validation:** uno o piu' campi `is_required` mancano
  o hanno valore invalido. La response include `errors[]` con il dettaglio.
- **External id mancante nel POST:** `metadata.external_id` e' obbligatorio.
  Senza, ottieni `400`.
- **Dipende dalla scadenza dei dati ADE:** alcune visure scadono dopo X
  giorni. Il backend lo segnala con un `warning` nella response, non un
  errore: leggilo e gestiscilo nel tuo UI.
- **Body troppo grande:** il POST con tutto il `form` puo' arrivare
  facilmente a 50-100 KB. Verifica i limiti del tuo HTTP client.

---

## 8. Test in sandbox

Endpoint sandbox: `https://staging.dokicasa.it/api/...` (richiedi accesso a
integrations@dokicasa.it). In sandbox:

- Nessuna fattura emessa
- Nessuna registrazione su ADE
- Wallet illimitato (`MAX_INT`)
- Webhook con suffisso `_TEST` (es. `PRACTICE_CREATED_TEST`)

Token di sandbox = `tok_test_...` invece di `tok_live_...`.

---

## 9. Checklist finale per Claude

Quando hai finito di integrare, verifica che:

- [ ] Il `DOKICASA_TOKEN` e' letto da una variabile d'ambiente, non hard-coded
- [ ] Lo stesso token non compare in NESSUN file del frontend
- [ ] Per ogni chiamata API ho gestito sia `2xx` sia `4xx/5xx`
- [ ] L'`external_id` che passi e' stabile per quell'utente (non cambia tra una sessione e l'altra)
- [ ] Salvi `bundle_user_service_id` nel DB partner per poter fare update successivi
- [ ] L'endpoint webhook valida `request_id` per evitare duplicati
- [ ] L'endpoint webhook risponde sempre `2xx` (anche su duplicati)
- [ ] Hai testato il flusso completo in sandbox prima di andare in prod
- [ ] Hai documentato come rigenerare il token in caso di compromissione

Se manca anche uno solo di questi punti, **NON considerare l'integrazione completa**.

---

## 10. Per integrare il FORM nel BROWSER (non solo le API)

Se invece di parlare server-to-server vuoi mostrare il form al tuo utente
finale dentro al tuo sito, usa il Web Component SDK:

- Documentazione: <https://sdk.dokicasa.it/>
- Prompt AI dedicato: <https://sdk.dokicasa.it/CLAUDE.md>

I due approcci sono complementari: tipicamente il partner usa l'SDK
nel browser per la compilazione e le API server-to-server per pollare/
sincronizzare lo stato delle pratiche col proprio CRM.

---

## 11. Riferimenti

- **OpenAPI spec:** <https://api.dokicasa.it/assets/swagger-concordato.yaml>
- **Swagger UI:** <https://api.dokicasa.it/api-doc/>
- **Forms Explorer:** <https://api.dokicasa.it/api-doc/form.html>
- **Pannello partner:** <https://dokicasa.it/partner>
- **Email integration team:** <integrations@dokicasa.it>