Panoramica
L’API v1 di FixNoty è un’interfaccia HTTP versionata, in sola lettura, per esportare tutte le segnalazioni di un singolo progetto. È pensata per script, CI, tool interni e automazioni successive — anche un agent AI che ha bisogno dello stesso contesto di risoluzione che vede una persona in dashboard.
L’API non cambia stato, non scrive commenti, non elimina oggetti e non restituisce path di storage grezzi né materiale segreto. I media arrivano come URL firmati a tempo. Il session replay arriva come chunk rrweb oppure come link firmato di 24 ore allo stesso player interattivo della tab Replay in dashboard — non come un nuovo MP4.
- Protocollo: HTTPS JSON. Non si usano body di richiesta (tutti i metodi sono GET).
- Prefisso: /api/v1. I prefissi di lingua come /it o /en valgono solo per il sito; non fanno parte del path API.
- Scope: un progetto per chiave. Un id di un altro progetto risponde 404 con code not_found.
- Compatibilità: gli enum di stato, priorità e categoria sono le stringhe italiane del database (es. aperto). Nei client non vanno tradotti.
URL di base e versioning
Host di produzione: https://www.fixnoty.com. In sviluppo usa l’origine del tuo deploy. Concatena il prefisso senza slash finale:
http
https://www.fixnoty.com/api/v1/feedbackI breaking change escono sotto un nuovo prefisso (/api/v2). In v1 possono comparire campi JSON aggiuntivi senza cambio di versione; i client devono ignorare le chiavi sconosciute. In v1 non c’è un documento OpenAPI.
Autenticazione
Autenticati con una chiave API di progetto. La chiave vale per un solo progetto e dà accesso in sola lettura. Autentica il progetto, non un utente: chiunque abbia il secret (script, collaboratore, integrazione) può leggere tutte le segnalazioni di quel progetto.
Invia il secret con esattamente uno di questi header:
http
Authorization: Bearer fn_live_…
X-Api-Key: fn_live_…Se sono presenti entrambi, vince Authorization: Bearer. Chiavi in query string, cookie e body vengono ignorate.
| Campo | Tipo | Descrizione |
|---|---|---|
| Format | string | fn_live_ + 64 caratteri esadecimali minuscoli in produzione, oppure fn_test_ + 64 hex in sviluppo. Regex: ^fn_(live|test)_[a-f0-9]{64}$. |
| Storage | sha256 | In database si salva solo l’hash SHA-256 del secret. Il valore in chiaro si vede una sola volta in Impostazioni progetto → API e non è recuperabile. |
| Prefix | string | In dashboard compare fn_live_ / fn_test_ più i primi 8 caratteri hex, così riconosci la chiave senza il secret. |
| Lifecycle | owner | Le chiavi API sono incluse nel piano Agency (e negli account demo). Prova gratuita e Pro non possono crearle; le chiavi già esistenti rispondono 403 plan_required. Solo il proprietario del progetto crea e revoca le chiavi. Al massimo 20 chiavi attive per progetto. La revoca è immediata: le richieste successive rispondono 401 invalid_api_key e i link del player 24h legati a quella chiave smettono di funzionare. |
| last_used_at | datetime | Aggiornato al massimo una volta al minuto dopo una richiesta riuscita. Se manca, la chiave non è mai stata usata con successo. |
Errori
Le risposte di errore usano un JSON stabile. code è sempre in inglese ed è il campo su cui fare lo switch. error è un messaggio leggibile e può essere localizzato da Accept-Language (it, oppure fallback en).
json
{
"error": "Invalid or revoked API key.",
"code": "invalid_api_key"
}| Campo | Tipo | Descrizione |
|---|---|---|
| 401 missing_api_key | auth | Manca sia Authorization: Bearer sia X-Api-Key. |
| 401 invalid_api_key | auth | Chiave malformata, sconosciuta o revocata. Stesso code nei tre casi, così non si può sondare se una chiave esiste. |
| 400 invalid_request | query | Query non valida (status, feedback_type, updated_since, limit o cursor). |
| 404 not_found | scope | Il progetto o la segnalazione non esiste, l’id non è un UUID, oppure appartiene a un altro progetto. Le sonde cross-project sono indistinguibili da un id assente. |
| 429 rate_limited | throttle | Troppe richieste. Retry-After indica i secondi finché la finestra scorrevole ha di nuovo capienza. |
| 403 plan_required | plan | Il piano del proprietario non include l’API. C’è su Agency (e in demo); non su prova gratuita né su Pro. |
| 503 database_unavailable | infra | L’API non raggiunge il database configurato. |
Un token del player malformato, scaduto o legato a una chiave revocata non è un errore v1: la pagina watch e lo stream eventi rispondono 404 o 410 e non fanno parte del contratto versionato.
Rate limit
Finestre scorrevoli di 60 secondi, contate in memoria per istanza del server (MVP; non è una quota Redis globale). Valgono entrambi i limiti:
- 60 richieste / minuto / IP client, valutate prima di parsare la chiave. Anche chi non è autenticato consuma questo budget.
- 120 richieste / minuto / chiave autenticata, valutate dopo il lookup riuscito della chiave.
Superato uno dei due limiti si ottiene 429 con code rate_limited e header Retry-After (secondi, minimo 1). L’accesso API è incluso in Agency, non è un piano a parte.
Header e cache
Ogni risposta JSON v1 include:
http
Cache-Control: private, no-storeGli URL dei media firmati e i link del player non devono finire in cache CDN o cache HTTP condivise. Il client può tenere la risposta in memoria locale fino a expires_at, poi deve richiamare GET /feedback/:id/assets (o /replay) per un URL nuovo.
I secret non vengono mai scritti nei log applicativi. Non loggare Authorization, X-Api-Key, token del player o le query string degli URL firmati.
Paginazione a cursore
GET /feedback usa keyset pagination su (updated_at DESC, id DESC). Non usa SQL OFFSET.
- Dimensione pagina di default 50; massimo 100. I valori sopra il massimo vengono tagliati, non rifiutati.
- Se esiste un’altra pagina, has_more è true e next_cursor è un blob opaco in base64url. Rimandalo come cursor.
- Un cursore malformato dà 400 invalid_request. Non inventare i cursori: non sono un linguaggio di query pubblico.
File firmati
I binari non sono mai inline nel JSON e l’API non restituisce uno zip del progetto. Ogni file è un oggetto:
json
{
"url": "https://…/object/sign/…?token=…",
"expires_in": 1800,
"expires_at": "2026-10-02T10:30:00.000Z"
}| Campo | Tipo | Descrizione |
|---|---|---|
| screenshot, widget attachments, debug attachments | TTL 1800s | 30 minuti dal momento in cui la risposta viene emessa. |
| rrweb manifest + chunks, 24h player watch URL | TTL 86400s | 24 ore. Riusa gli oggetti già salvati per il player in dashboard; nessuno step di encode extra. |
Fai GET direttamente sull’url. Dopo la scadenza lo storage rifiuta il token; richiama assets o replay. Le righe allegato includono anche id, file_name, mime_type, file_size e created_at.
Session replay
La cattura nativa è eventi DOM rrweb, non una registrazione schermo. La v1 espone due formati di prima classe più un video legacy opzionale:
- formats.player — pagina HTML firmata che monta lo stesso player rrweb della tab Replay in dashboard. Valida 24 ore. Zero storage extra.
- formats.rrweb — manifest JSON firmato + file chunk in ordine (24 ore). Concatena gli array di eventi dei chunk e passali a un player rrweb.
- formats.video — presente solo se sulla riga esiste già un MP4 storico. La v1 non transcodifica rrweb in MP4. Stato tipico: not_generated.
formats.still è lo screenshot della segnalazione (TTL 30 minuti). timeline è il log azioni del widget (click, navigazione, input mascherati, errori JS) — un array JSON più piccolo, distinto da rrweb.
Le segnalazioni senza registrazione rispondono comunque HTTP 200 con has_replay: false. HTTP 404 significa che la segnalazione non appartiene al progetto della chiave.
Sicurezza del player 24 ore
- Emesso solo da GET /api/v1/feedback/:id e …/replay autenticati, per una segnalazione dello stesso progetto della chiave.
- HMAC-SHA256 su un payload con scadenza. I token sono opachi: i client non devono parsarli.
- Le claim includono id progetto, id segnalazione, id chiave API e scadenza Unix. Revocare la chiave invalida tutti i link player già emessi, anche prima delle 24 ore.
- Token scaduti, falsi o revocati → 404/410. La pagina watch non richiede login in dashboard (così un browser o un agent con browser può aprirla). Avere l’URL è la credenziale, fino a scadenza o revoca.
- Header di risposta: Cache-Control: private, no-store, Referrer-Policy: no-referrer, X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none', X-Robots-Tag: noindex, nofollow, X-Content-Type-Options: nosniff. Il player non si può mettere in iframe su un altro sito e non è offerto ai motori di ricerca.
Il player HTML carica gli eventi da uno stream interno GET /api/replay-watch/:token (limitato per IP, stessi header di sicurezza). Quella rotta non fa parte del contratto v1; integra su formats.player.watch.url o formats.rrweb.
GET /api/v1/project
Conferma la chiave e restituisce il progetto a cui è legata. Usala come verifica di connettività prima di elencare le segnalazioni.
json
{
"project": {
"id": "11111111-2222-4333-8444-555555555555",
"name": "Client site",
"site_url": "https://staging.example.com"
}
}| Campo | Tipo | Descrizione |
|---|---|---|
| project.id | uuid | Progetto legato alla chiave. |
| project.name | string | Nome visibile. |
| project.site_url | string | URL del sito configurato. |
GET /api/v1/feedback
Indice leggero delle segnalazioni del progetto. Non include mai recording_events, console_logs, conversation, activity né URL firmati dei file. Per quelli usa dettaglio / assets / replay.
Parametri query
| Campo | Tipo | Descrizione |
|---|---|---|
| status | string | Default open. open o aperte = aperto + in_lavorazione + in_attesa_cliente. all o tutti = tutti gli stati. Altrimenti una lista di stati database separati da virgola. |
| feedback_type | enum | visual | debug_replay | text_content | request |
| updated_since | ISO 8601 | Limite inferiore incluso su updated_at. Timestamp non validi → 400. |
| limit | int | 1–100, default 50. Non interi e valori < 1 → 400. |
| cursor | opaque | next_cursor della pagina precedente. |
| include | ignored | Viene parsato, ma i JSONB pesanti (recording_events) non vengono mai dumpati sull’endpoint lista. |
json
{
"data": [
{
"id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
"pin_number": 4,
"status": "aperto",
"priority": "bloccante",
"category": "bug",
"feedback_type": "debug_replay",
"title": "Il checkout non invia l’ordine",
"comment": "Clicco Paga e non succede nulla.",
"client_title": null,
"page_url": "https://staging.example.com/checkout",
"page_path": "/checkout",
"created_at": "2026-10-01T14:12:00.000Z",
"updated_at": "2026-10-01T15:40:00.000Z",
"last_activity_at": "2026-10-01T15:40:00.000Z",
"author_name": "Anna",
"assigned_to": null,
"has_screenshot": true,
"has_replay": true,
"has_attachments": true,
"has_console_errors": true,
"attachment_count": 1
}
],
"next_cursor": null,
"has_more": false
}| Campo | Tipo | Descrizione |
|---|---|---|
| id | uuid | Id della segnalazione. |
| pin_number | number | Numero pin sulla pagina. |
| status | enum | aperto | in_lavorazione | in_attesa_cliente | risolto | archiviato |
| priority | enum | normale | importante | bloccante |
| category | enum | testo | design | immagine | bug | mobile | contenuto | altro |
| feedback_type | enum | visual | debug_replay | text_content | request |
| title | string | null | Titolo in dashboard. |
| comment | string | Commento del cliente. |
| client_title | string | null | Titolo del brief «nuova richiesta». |
| page_url | string | URL completo della pagina al momento della cattura. |
| page_path | string | Pathname ricavato da page_url (query e hash rimossi). |
| created_at / updated_at | datetime | ISO 8601 UTC. updated_at guida il cursore. |
| last_activity_at | datetime | null | Ultima attività in dashboard o portale. |
| author_name | string | Nome visibile del cliente. |
| assigned_to | object | null | { user_id, name, email } se assegnata. |
| has_screenshot / has_replay / has_attachments / has_console_errors | boolean | Flag leggeri per decidere se chiamare dettaglio/replay. |
| attachment_count | number | Numero di allegati widget (non i file debug). |
| next_cursor | string | null | Cursore opaco, oppure null sull’ultima pagina. |
| has_more | boolean | Se esiste un’altra pagina. |
GET /api/v1/feedback/:id
Contesto completo per capire (e un giorno risolvere) una segnalazione del progetto della chiave. La query opzionale include=recording_events aggiunge la timeline del widget. Gli eventi rrweb non sono mai inline: usa gli URL firmati in replay.
http
GET /api/v1/feedback/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee?include=recording_events
Authorization: Bearer $FIXNOTY_API_KEYIdentità
| Campo | Tipo | Descrizione |
|---|---|---|
| id | uuid | Id della segnalazione. |
| project_id | uuid | Sempre il progetto legato alla chiave. |
| pin_number | number | Numero pin. |
| status / priority / category / feedback_type | enum | Stesse enumerazioni dell’endpoint lista. |
| created_at / updated_at / last_activity_at | datetime | Timestamp UTC. |
| resolved_at / archived_at | datetime | null | Impostati quando la segnalazione esce dal set aperto. |
Testo / brief
| Campo | Tipo | Descrizione |
|---|---|---|
| title / comment | string | Il titolo può essere null; il commento è il testo principale del cliente. |
| user_goal / actual_result / expected_result | string | null | Campi bug strutturati, se il cliente li ha compilati. |
| reproducibility / impact | string | null | Contesto opzionale di gravità. |
| affected_devices | string[] | null | Dispositivi citati dal cliente. |
| client_title / client_description | string | null | Brief «nuova richiesta». |
| waiting_for_customer_reason | string | null | Perché la segnalazione è in attesa del cliente. |
Dove
| Campo | Tipo | Descrizione |
|---|---|---|
| page_url / page_path | string | Dove è stato messo il pin. |
| position | object | { x, y, x_percent, y_percent } coordinate click/pin. |
| selection | object | { type: point | area | none, rect_*_percent } rettangolo selezionato. |
| scroll | object | { x, y } offset di scroll. |
| viewport | object | { width, height } viewport di layout al momento della cattura. |
Ambiente
| Campo | Tipo | Descrizione |
|---|---|---|
| environment.user_agent | string | null | Stringa UA grezza. |
| environment.browser / device | string | null | Browser e classe dispositivo rilevati. |
| environment.recording_metadata | object | null | viewport_width/height, visual_viewport_*, device_pixel_ratio, orientation, screen_*, touch_support, pointer_events, device_class, platform, pages_visited, event_count, replay_segment_count, session_id. I secret sono rimossi. |
Persone
| Campo | Tipo | Descrizione |
|---|---|---|
| author | object | { name, email }. L’email può essere null. |
| assigned_to | object | null | { user_id, name, email }. |
| visible_to_client | boolean | Se il portale cliente mostra questa segnalazione. |
| client_confirmed_at / client_reopened_at | datetime | null | Conferma / riapertura dal portale. |
Media
| Campo | Tipo | Descrizione |
|---|---|---|
| screenshot | signed | null | JPEG/PNG/WebP firmato 30 minuti. screenshot_captured_at è quando è stato scattato. |
| attachments[] | array | File widget con URL firmati (30 min). |
| debug_attachments[] | array | File catturati durante Registra problema (30 min). |
| replay | object | null | Vedi il pacchetto replay. playback è sempre “rrweb”. video è un alias di formats.video.file se esiste un MP4 legacy. |
Debug, conversazione, flag
| Campo | Tipo | Descrizione |
|---|---|---|
| console_logs | array | { type: error | unhandledrejection, message, stack?, url?, timestamp_ms }. |
| recording_events | array | null | Timeline widget. null se non passi include=recording_events. |
| recording_warnings | array | { code, message } dalla cattura. |
| debug_summary / captured_values | object | array | null | Payload debug avanzato sanitizzato. Le chiavi sensibili sono rimosse in ricorsione. |
| conversation[] | array | Note team + commenti cliente, in ordine di data. { id, body, created_at, visibility: internal | client, source: team | client, author_name }. |
| activity[] | array | { id, action, old_value, new_value, created_at }. action è una delle enumerazioni attività sotto. created, status_changed, priority_changed, category_changed, internal_notes_updated, customer_update_added, waiting_reason_updated, feedback_resolved, feedback_archived, client_comment_added, client_confirmed_resolution, client_reopened_task, visibility_changed, screenshot_captured, replay_attached, file_attached, client_context_added, title_updated, assignee_changed. |
| flags | object | has_screenshot, has_replay, has_attachments, has_console_errors, has_advanced_data, has_debug_files. |
GET /api/v1/feedback/:id/assets
Riemette gli URL firmati senza riscaricare commenti, conversazione o attività. Il expires_in in cima è il TTL di screenshot/allegati (1800s). replay_expires_in è il TTL di player/rrweb (86400s).
json
{
"data": {
"screenshot": {
"url": "https://….supabase.co/storage/v1/object/sign/feedback-screenshots/…?token=redacted",
"expires_in": 1800,
"expires_at": "2026-10-02T10:30:00.000Z"
},
"attachments": [],
"debug_attachments": [],
"replay": null
},
"expires_in": 1800,
"replay_expires_in": 86400
}GET /api/v1/feedback/:id/replay
Busta dedicata al replay. La timeline del widget è inclusa di default. Apri formats.player.watch.url nel browser per vedere la sessione come in dashboard.
json
{
"data": {
"feedback_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
"has_replay": true,
"duration_seconds": 42,
"recording_session_id": null,
"debug_session_id": "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff",
"expires_in": 86400,
"formats": {
"rrweb": {
"available": true,
"playback": "rrweb",
"expires_in": 86400,
"manifest": {
"url": "https://….supabase.co/storage/v1/object/sign/debug-replays/…?token=redacted",
"expires_in": 86400,
"expires_at": "2026-10-03T10:00:00.000Z"
},
"chunks": [
{
"url": "https://…/chunk-000.json?token=redacted",
"expires_in": 86400,
"expires_at": "2026-10-03T10:00:00.000Z"
}
]
},
"player": {
"available": true,
"playback": "player",
"expires_in": 86400,
"watch": {
"url": "https://www.fixnoty.com/it/replay/<signed-token>",
"expires_in": 86400,
"expires_at": "2026-10-03T10:00:00.000Z"
}
},
"video": {
"available": false,
"playback": "mp4",
"status": "not_generated",
"mime_type": "video/mp4",
"expires_in": 86400,
"file": null
},
"still": {
"url": "https://….supabase.co/storage/v1/object/sign/feedback-screenshots/…?token=redacted",
"expires_in": 1800,
"expires_at": "2026-10-02T10:30:00.000Z"
}
},
"timeline": [
{
"t": 1200,
"at": "2026-10-01T14:12:01.200Z",
"type": "click",
"page_url": "https://staging.example.com/checkout"
}
],
"warnings": []
},
"expires_in": 86400
}Come usare i chunk rrweb
- GET dell’URL firmato del manifest.
- GET di ogni URL chunk, in ordine di array.
- Concatena gli array di eventi.
- Passa il risultato a un player rrweb. Non è un MP4.
Enumerazioni
status
aperto | in_lavorazione | in_attesa_cliente | risolto | archiviato
priority
normale | importante | bloccante
category
testo | design | immagine | bug | mobile | contenuto | altro
feedback_type
visual | debug_replay | text_content | request
recording event type
widget_loaded · widget_reconnected · click · tap · pointer_down · pointer_up · scroll · navigation · input_focus · input_change_masked · password_field_interaction_masked · keydown_masked · file_selection_masked · console_error · js_error · unhandled_rejection · visibility_change · pagehide · beforeunload · debug_started · debug_stopped · replay_started · replay_stopped · replay_snapshot · input_value_captured · input_value_masked · dropdown_open · dropdown_selection · picker_open · picker_selection · file_selected · file_attached · file_attachment_skipped · orientation_change · viewport_resize · visual_viewport_resize · tab_open
Elementi timeline: { t, at, type, page_url?, from?, to?, x?, y?, … }. Password e input sensibili sono mascherati in cattura (tipi *_masked).
Consumo da un sistema AI
Questa API è un canale dati. Non chiama un modello e non sistema le segnalazioni da sola.
- LLM solo testo. Legge lista + dettaglio JSON, console, timeline e conversazione. Ottiene azioni, errori, copy e un fotogramma. Non ottiene il movimento.
- Agent con browser. Apre formats.player.watch.url, preme play e guarda o screenshotta il DOM ricostruito. Un GET HTTP nudo su quell’URL restituisce l’HTML/JS del player, non i fotogrammi.
- Player proprio. Scarica i chunk formats.rrweb. Sono gli stessi eventi della dashboard.
Non generiamo MP4 da rrweb: duplicerebbe storage già tenuto come chunk di eventi. Il player 24 ore riusa quei chunk.
Modello di sicurezza
- Questo URL di documentazione è pubblico di proposito. Le docs pubbliche descrivono il contratto. Non danno accesso.
- Il controllo accessi è la chiave API di progetto (hashata a riposo) più i filtri server-side su project_id. Indovinare l’UUID di una segnalazione di un altro progetto non la fa trapelare.
- RLS in dashboard: solo il proprietario può leggere/inserire/aggiornare le righe di project_api_keys.
- I link del player sono unlisted, non indicizzati, non embeddabili, a tempo e revocabili con la chiave. Non inoltrarli.
- Non mettere le chiavi API nei bundle frontend né in pagine che leakano il Referer. Preferisci caller server-side.
Esempi curl
bash
export FIXNOTY_API_KEY='fn_live_…'
export APP_URL='https://www.fixnoty.com'
# Verifica la chiave
curl -sS "$APP_URL/api/v1/project" \
-H "Authorization: Bearer $FIXNOTY_API_KEY"
# Segnalazioni aperte (default)
curl -sS "$APP_URL/api/v1/feedback?status=open&limit=50" \
-H "Authorization: Bearer $FIXNOTY_API_KEY"
# Tutti gli stati dal 1° ottobre 2026
curl -sS "$APP_URL/api/v1/feedback?status=all&updated_since=2026-10-01T00:00:00.000Z" \
-H "Authorization: Bearer $FIXNOTY_API_KEY"
# Dettaglio completo + timeline widget
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID?include=recording_events" \
-H "Authorization: Bearer $FIXNOTY_API_KEY"
# Solo media firmati (refresh)
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID/assets" \
-H "X-Api-Key: $FIXNOTY_API_KEY"
# Pacchetto replay: player 24h + rrweb + still + timeline
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID/replay" \
-H "Authorization: Bearer $FIXNOTY_API_KEY"Versioning e fuori scope
Versione attuale: v1. Possono comparire campi aggiuntivi. Restano esplicitamente fuori dalla v1:
- POST / PATCH / DELETE (stato, commenti, assegnazione)
- Zip sincrono di tutto il progetto
- Billing, piani a pagamento, webhook
- SDK ufficiali e OpenAPI
- Inferenza di modelli, auto-fix o webhook AI in uscita
- Transcodifica MP4 da rrweb
Riferimento pubblico canonico: questa pagina. Una nota tecnica allineata sta nel repository in docs/api-v1.md. Stampa dal browser («Stampa → Salva come PDF») è supportata; non spediamo un PDF versionato a parte che può divergere dall’implementazione.