Documentazione dell'API
Contratto 1.0 · indirizzo https://api.agileascolto.agile.software · le novità sono in Novità e in GET /health.
Autenticazione
Ogni richiesta porta la chiave nell'header Authorization: Bearer aga_… (oppure X-API-Key). Nel WebSocket, se il client non può mandare header (browser), si usa il sottoprotocollo agileascolto, key.aga_…. La chiave non va mai nell'URL.
Ogni chiave porta uno o più scope — transcribe (file), stream (WebSocket), batch (lavori asincroni), metrics (misure e quadro), test (scope aggiuntivo, mai da solo: marca una chiave di prova, non copre nessuna rotta) — e può avere una scadenza opzionale: oltre, 403 KEY_EXPIRED. Per provare l'API senza onboarding esiste una chiave passe-partout di prova, a termine e con quota bassa.
Modelli
| Nome | Cosa è |
|---|---|
agileascolto-turbo | Modello generale per l'italiano, predefinito. Streaming e file. |
agileascolto-discovery | Modello in valutazione per il telefono (nomi, numeri, codici). Abilitato per chiave, su richiesta. |
whisper-1 | Accettato solo come alias di compatibilità del modello predefinito, per i client che lo mandano per abitudine. |
GET /v1/models elenca i modelli che la tua chiave può usare.
Trascrizione da file — POST /v1/audio/transcriptions
Compatibile con l'API OpenAI. Corpo multipart/form-data: file (wav, mp3, m4a, ogg, webm, flac… fino a 200 MB e 60 minuti), model, language (predefinito it), response_format = json | text | verbose_json | srt | vtt (sottotitoli: al massimo due righe da 42 caratteri e 7 secondi per didascalia; le frasi lunghe si spezzano ai confini di parola). words=1 (o timestamp_granularities[]=word, come OpenAI): parole con inizio e fine in verbose_json, e sottotitoli tagliati sulle pause fra le parole. prompt e temperature si accettano e si ignorano. codici=1: i codici dettati (IBAN, POD, PDR, codice fiscale, numeri di pratica letti lettera per lettera) escono compattati in un solo token: «i ti otto uno di zero uno sette zero…» → IT81D0170…, «bi esse trattino uno cinque…» → BS-15767. Il resto della frase non cambia. La risposta porta anche codici: i token trovati col tipo, valido (checksum mod 97 per l'IBAN, carattere di controllo per il codice fiscale, forma per gli altri) e motivo; se il codice non regge e c'è una sola riparazione che lo farebbe reggere, anche riparato e regola — valido resta false: è una proposta da far confermare, non un codice accettato. atteso=cf (con codici=1): il tipo di codice che hai chiesto tu (iban|cf|pod|pdr|telefono|pratica) — il candidato che risponde alla domanda porta atteso: true, e se l'orecchio ha perso un carattere e nessun candidato ha più quella forma il più vicino viene adottato e ricontrollato come quel tipo (adottato: true), col motivo da dire. Un candidato valido non si adotta mai e l'adozione non rende valido niente. ricomponi=<codice sentito prima> + blocco=cognome (con codici=1 e atteso): quando il codice non regge e non c'è una riparazione, chiedi un blocco invece di tutto (codice fiscale: cognome, nome, nascita, comune, controllo; IBAN: paese, banca, sportello, conto; POD e PDR: testa, coda) e manda il turno della risposta con quei due campi: torna ricomposto col codice rimesso insieme e ricontrollato. giudicabile dice se il controllo di quel tipo ha un checksum e può quindi bocciare una ricomposizione sbagliata (IBAN e codice fiscale sì, POD e PDR no: là valido: true vuol dire solo «la forma regge» e il codice va riletto alla persona). Il blocco lo scegli tu: il fronte non sa dove sia l'errore. Chiedine uno per volta nell'ordine del codice e lascia che sia il controllo a dirti quando fermarti: il primo innesto riporta il codice alla lunghezza giusta, e da lì la scala chiude su 30 codici fiscali su 36 e 29 IBAN su 29 senza ridettare niente per intero (mediana 4 domande da 3–5 caratteri, 0 ricomposizioni sbagliate accettate). Conviene perché il pezzo corto si sente: un codice fiscale dettato intero esce esatto 9 volte su 90, uno da 7–10 caratteri sta fra il 74 % e il 93 %.
GET /v1/codici/blocchi?tipo=cf&sentito=<codice sentito prima> dà i blocchi di quel tipo con la frase da dire e, per ciascuno, posizionabile: un blocco si innesta solo se il codice sentito è lungo abbastanza, e chiederne uno che non si innesta butta via un turno. Senza sentito torna la sola tavola dei blocchi.
from openai import OpenAI
c = OpenAI(base_url="https://api.agileascolto.agile.software/v1", api_key="aga_…")
r = c.audio.transcriptions.create(model="agileascolto-turbo", file=open("chiamata.wav", "rb"), language="it")
print(r.text)
curl https://api.agileascolto.agile.software/v1/audio/transcriptions \
-H "Authorization: Bearer aga_…" -F file=@chiamata.wav -F model=agileascolto-turbo -F language=it
{"text": "…"}
Con verbose_json: {"task","language","duration","text","segments":[{"id","start","end","text"}],"model","processing_ms"}. I segmenti sono le frasi come le ha chiuse il motore, con inizio e fine in secondi. Con words=1 c'è anche "words":[{"word","start","end"}], in cima (come OpenAI) e dentro ogni segmento. Per i lavori lunghi (scope batch), POST /v1/audio/jobs accetta il file e GET /v1/audio/jobs/{id}?format=srt (o vtt, text) consegna il file pronto.
Streaming — WS /v1/audio/stream?model=…
Il client apre il WebSocket, manda un messaggio di avvio, poi l'audio PCM a 16 bit mono in frame da 20 ms, e riceve parziali e finali mentre parla.
| Direzione | Messaggio | Significato |
|---|---|---|
| client → server | {"type":"start","language":"it","sample_rate":16000} | Avvio. sample_rate 8000 (telefono) o 16000. Facoltativo "codici":true (o ?codici=1 nell'URL): i final escono coi codici dettati compattati, i partial no. |
| server → client | {"type":"started","model":"agileascolto-turbo"} | Pronto a ricevere audio. model dice quale orecchio ascolta (ob. 6): il nome nostro, quello del registro e di /v1/usage. |
| client → server | frame binari PCM s16le mono | 20 ms per frame (640 byte a 16 kHz, 320 a 8 kHz), a ritmo reale. |
| server → client | {"type":"partial","text":"…"} | Testo provvisorio della frase in corso. |
| server → client | {"type":"final","text":"…","speech_final":true} | Frase chiusa. speech_final vero = la persona ha finito di parlare. |
| client → server | {"type":"flush"} | Chiudi subito la frase in corso (fine turno decisa dal client), la sessione resta aperta. |
| client → server | {"type":"stop"} | Fine sessione: arriva l'ultimo finale, poi {"type":"closed"}. |
| server → client | {"type":"error","code":"…","status":429,"message":"…"} | Errore del fronte, poi chiusura con codice 1008. |
| server → client | {"type":"closing","code":"SHUTTING_DOWN","retry_after_s":10} | Il fronte si sta fermando (un ridispiego): subito dopo arriva il finale di ciò che hai già detto, poi la chiusura col codice 1012 («il servizio riparte»). Riapri la sessione dopo retry_after_s: le librerie WebSocket riprovano da sole sul 1012. |
import asyncio, json, websockets
async def main():
async with websockets.connect("wss://api.agileascolto.agile.software/v1/audio/stream?model=agileascolto-turbo",
additional_headers={"Authorization": "Bearer aga_…"}) as ws:
await ws.send(json.dumps({"type": "start", "language": "it", "sample_rate": 16000}))
# … await ws.send(frame_pcm) ogni 20 ms …
await ws.send(json.dumps({"type": "stop"}))
async for m in ws:
print(m)
asyncio.run(main())
Consumi — GET /v1/usage
La chiave vede solo i propri consumi: per mese, operazione (transcribe, stream, batch) e modello, con richieste, secondi di audio e millisecondi di elaborazione. Il campo used_s_this_month si confronta con quota_s_month. Senza scrivere codice: la pagina d'uso mostra gli stessi numeri incollando la chiave (che viaggia solo nell'intestazione, mai nell'indirizzo).
Quadro — GET /v1/quadro
Per chi vigila (chiave con scope metrics): le chiavi con richieste, secondi e percentuale di quota del mese (?mese=AAAA-MM, predefinito il corrente), i totali e lo stato dei motori. Il perimetro è quello della chiave e sta scritto nella risposta (perimetro): una chiave con un tenant vede la sua azienda, una chiave della regia (senza tenant) il servizio intero. I contatori di /metrics sono del servizio e non si restringono a un'azienda: a una chiave con un tenant rispondono 403 METRICS_TENANT_FORBIDDEN. Solo numeri: mai segreti, mai testi. Lo stesso quadro si legge sul server con agileascolto-keys.py quadro e, con una chiave metrics, nella pagina d'uso.
Stato — GET /health
Senza chiave. Dice se il fronte e i motori rispondono, la versione del contratto, i flussi attivi e il link alle novità.
Console self-service — /v1/console
Rotte a parte dal contratto di trascrizione: non si autenticano con una chiave aga_… ma con un token utente agt_… del tuo tenant (Authorization: Bearer agt_…). Servono all'amministratore di un'azienda (o a una persona individuale) per gestire da solo le proprie chiavi e le proprie persone, senza chiedere niente a noi. Vedi e tocchi solo il tuo tenant (403 TENANT_FORBIDDEN); ogni operazione finisce nel registro, mai col segreto di una chiave né col token di un invito.
| Rotta | Cosa fa |
|---|---|
POST /v1/console/keys | Emette una chiave del tuo tenant. Il segreto si vede una volta sola, qui. 403 PLAN_EXCEEDED se la chiave da sola prometterebbe più del piano dell'azienda. Self-service sono transcribe, stream e batch: metrics e test li emette la regia (403 SCOPE_NOT_SELF_SERVICE). |
GET /v1/console/keys | Le tue chiavi con stato e uso del mese, più il piano dell'azienda e i suoi secondi. Ognuna dice di chi è: owner_email e owner_stato accanto all'owner (una chiave attiva di una persona sospesa si vede a occhio); ?owner= restringe a una persona (id o email) o a un prodotto. Mai il segreto, mai il token di una persona. |
POST /v1/console/keys/{key_id}/rotate | Chiave nuova con gli stessi diritti; grazia_ore tiene viva la vecchia per il periodo di grazia. |
POST /v1/console/keys/{key_id}/revoke | Revoca immediata. |
POST /v1/console/keys/revoke-by-owner | Toglie in una mossa tutte le chiavi attive di una persona o di un prodotto (owner: id, email o nome del prodotto) — è il gesto per chi ha lasciato l'azienda. Senza grazia: una revoca non consegna un segreto nuovo, quindi chi vuole tenere viva l'integrazione ruota la chiave. 404 OWNER_NOT_FOUND se quel nome non ha nessuna chiave. |
POST /v1/console/users | Invita una persona nel tuo tenant (email, ruolo, scadenza_giorni). Il token dell'invito si vede una volta sola, qui, e scade: 7 giorni di serie, 90 il massimo (expires_at nella risposta). 409 USER_EXISTS se è già una tua persona. |
POST /v1/console/users/accept | Chi ha ricevuto un invito lo accetta col proprio token: non si accetta per conto di altri. |
GET /v1/console/users | Le tue persone con ruolo (amministratore, utente) e stato (invitato, attivo, sospeso). |
POST /v1/console/users/{utente_id}/suspend | Sospende una persona: il suo token smette di entrare in console. Le sue chiavi non si toccano da sole, mai: chiavi nel corpo è la scelta — "lascia" (di serie: restano, e il registro dice quali) o "revoca" (le sue chiavi attive vanno via nella stessa mossa). 409 LAST_ADMIN sull'ultimo amministratore attivo: la tua azienda non può chiudersi fuori da sola. |
POST /v1/console/users/{utente_id}/reactivate | Riattiva chi torna. |
PUT /v1/console/users/{utente_id}/role | Cambia il ruolo. Invitare, sospendere e assegnare ruoli è dell'amministratore (403 ADMIN_REQUIRED); l'elenco lo legge chiunque del tenant. |
POST /v1/console/users/{utente_id}/rotate-token | Token nuovo per una persona, identità invariata (stesso id, stesso ruolo, stesse chiavi, stessa storia nel registro): si vede una volta sola, qui. Il tuo lo cambi tu, anche se sei un utente semplice; quello delle tue persone lo cambia un amministratore. Di serie il vecchio muore subito (grazia_ore: 0) e poi dà 403 TOKEN_ROTATED. 409 USER_NOT_ACTIVE se la persona è sospesa. |
GET /v1/console/audit | Registro delle operazioni del tuo tenant: chi, cosa, su quale chiave o su quale persona, con che esito. Una riga che parla di una tua chiave c'è anche quando l'ha scritta la regia, e dice di chi è chi ha agito. Filtri key_id, persona (id o email) e operazione; limite da 1 a 1000 (100 di serie), oltre è 400 AUDIT_LIMIT_INVALID — niente tagli silenziosi — e la risposta dice quante righe combaciano e se è troncato. Mai un token, mai un'impronta, mai un'email. |
GET /v1/console/tenants/{tenant}/piano | Il piano dell'azienda (richieste al minuto e secondi al mese concordati) e i suoi secondi del mese. |
Il piano dell'azienda — il tetto del tenant
rate_per_min e quota_s_month stanno sulla chiave, e le chiavi le fa l'amministratore dalla console: sono una scelta tua, non il contratto. Il contratto è il piano dell'azienda (il piano del tenant): richieste al minuto e secondi al mese dell'azienda intera, somma di tutte le sue chiavi — le revocate comprese, per i secondi del mese.
Il piano lo scrive solo la regia (PUT /v1/console/tenants/{tenant}/piano, header X-Regia-Superadmin): dalla console un'azienda non si allarga il proprio tetto, perché è la parte concordata. Lo legge quando vuole: l'amministratore in GET /v1/console/keys (piano, tenant_used_s_mese) e in GET /v1/console/tenants/{tenant}/piano, chi possiede una chiave in GET /v1/usage (piano_tenant, tenant_used_s_this_month).
Morde in due punti. Alla creazione di una chiave che da sola prometterebbe più del piano: 403 PLAN_EXCEEDED, e vale anche per una chiave «senza limite». E a ogni richiesta, sulla somma di tutte le chiavi dell'azienda: 429 TENANT_RATE_LIMIT e 429 TENANT_QUOTA_EXCEEDED. Questi due dicono una cosa diversa dai loro omonimi della chiave: RATE_LIMIT e QUOTA_EXCEEDED sono di quella chiave (l'amministratore può darne un'altra o allargarla), i TENANT_* sono dell'azienda, e una chiave in più non serve a niente. Il ritmo al minuto è su una finestra scorrevole (gli ultimi 60 secondi), non sul minuto d'orologio.
0 vuol dire «nessun tetto», come per le chiavi, e un tenant senza piano scritto prende quello di partenza del dispiegamento: 0 e 0 di serie, quindi chi non usa i piani non cambia comportamento.
Perché esiste, misurato: col piano «6 richieste al minuto, 6 secondi al mese» e un amministratore che si fa 3 chiavi in buona fede dalla console, senza il piano l'azienda passava 18 richieste al minuto su un tetto di 6 (3,0x) e 18 secondi nel mese su una quota di 6 (3,0x); col piano, 6 e 6 (1,0x). È la misura del nostro banco di prova, non una promessa di capacità.
Chiedere una licenza — POST /v1/richieste-licenza
Rotta pubblica, senza chiave: la chiede chi ancora non ne ha una. Corpo JSON con nome, email e uso obbligatori, azienda, piano (prova, standard o enterprise, senza vale prova) e volume_stimato facoltativi. Il campo sito_web va lasciato vuoto: è la trappola contro i robot.
Risposta 201 col numero da citare quando ci si scrive: {"numero":7,"id":"req_1a2b3c4d","stato":"registrata","messaggio":"richiesta registrata n. 7"}.
Nessuna richiesta inghiottita: la riga si scrive prima di provare qualunque recapito, e dal momento in cui esiste la risposta è 201 qualunque cosa succeda dopo. Un giro sul nostro box la porta a chi deve leggerla e la segna fatta solo a consegna riuscita: se il recapito è giù, la richiesta resta e il giro dopo riprova.
I rifiuti hanno un nome: 400 LICENSE_REQUEST_INVALID (dice quale campo), 400 LICENSE_REQUEST_REJECTED (la trappola era piena: nessuna riga scritta), 413 LICENSE_REQUEST_TOO_LARGE (corpo oltre 4096 byte, controllato mentre si legge), 429 RATE_LIMIT col Retry-After (massimo 5 al minuto per indirizzo).
Privacy: dell'indirizzo conserviamo solo lo SHA-256, e nel nostro registro finiscono numero, id e piano — mai il nome, mai l'email, mai il testo di quello che hai scritto. L'elenco delle richieste (GET /v1/richieste-licenza) è solo nostro, e senza il token della regia configurato è chiuso anche a noi (403 SUPERADMIN_REQUIRED).
Errori
| Stato | Codice | Quando |
|---|---|---|
| 401 | KEY_MISSING, KEY_INVALID | Chiave assente o sconosciuta. |
| 403 | KEY_REVOKED, KEY_EXPIRED, SCOPE_NOT_ALLOWED, MODEL_NOT_ALLOWED | Chiave revocata, scaduta, o che non copre l'operazione o il modello. |
| 403 | INVITE_EXPIRED, TOKEN_ROTATED | In console: invito non accettato e scaduto, o token ritirato da una rotazione. Il segreto è stato riconosciuto — chiedine uno nuovo: non è un 401. |
| 404 | MODEL_UNKNOWN | Nome di modello inesistente. |
| 400 | AUDIO_INVALID, FORMAT_UNSUPPORTED | File non decodificabile o troppo corto; response_format (o format dei lavori) non previsto. |
| 501 | TIMESTAMPS_UNAVAILABLE | Sottotitoli chiesti a un modello il cui motore non dà i tempi dei segmenti: non si inventano. |
| 413 | AUDIO_TOO_LONG, SESSION_TOO_LONG, AUDIO_TOO_LARGE | Oltre 60 minuti per file o 4 ore per sessione; AUDIO_TOO_LARGE = corpo oltre i 200 MB. |
| 429 | RATE_LIMIT, QUOTA_EXCEEDED, TENANT_RATE_LIMIT, TENANT_QUOTA_EXCEEDED | Troppe richieste al minuto, o secondi del mese esauriti (anche a metà di uno streaming). I TENANT_* sono il tetto dell'azienda intera (il piano del tenant), non di questa chiave: farsene un'altra non serve. |
| 503 | ENGINE_DOWN, CAPACITY, SHUTTING_DOWN | SHUTTING_DOWN = il fronte si sta fermando (un ridispiego): non è un guasto e non è un pieno, torna fra pochi secondi, e chi era già dentro riceve prima il suo finale. Motore non raggiungibile, o troppi flussi in parallelo su quel motore (il messaggio lo nomina; oltre c'è il tetto di tutto il fronte), o coda dei lavori di quel motore piena — per numero o per secondi d'audio in RAM di tutto il fronte (il messaggio dice quanti sono liberi), o troppe richieste con audio in volo insieme (si aspetta il proprio turno, poi 503). Non c'è mai un ripiego silenzioso su un altro modello o su un altro servizio: se il modello sta su più macchine il fronte le prova tutte, e ENGINE_DOWN vuol dire che nessuna risponde. |
| 502 | ENGINE_ERROR | Il motore ha rifiutato la richiesta. |
Corpo degli errori: {"error":{"code":"…","message":"…"}}.
La quota (e il piano dell'azienda) conta anche l'audio entrato e non ancora registrato: una telefonata aperta, un file in lavorazione, un lavoro in coda. Tre linee insieme non passano tre volte la quota, e GET /v1/usage dice quei secondi in in_corso_s e tenant_in_corso_s. Il blocco d'audio rifiutato non arriva al motore e non si paga.
Quando ha senso riprovare, il rifiuto dice anche quando: i 429 e il 503 CAPACITY portano l'intestazione Retry-After (secondi interi) e lo stesso numero in error.retry_after_s — sul WebSocket, dove le intestazioni non esistono, solo il campo. Sul tetto al minuto è l'istante in cui si libera un posto, sulla quota è l'inizio del mese prossimo, sul pieno è un consiglio di pochi secondi, su un ridispiego (SHUTTING_DOWN) è il tempo di fermarsi e ripartire. Gli altri rifiuti non lo portano: riprovare non cambierebbe niente.
Privacy
L'audio viaggia in memoria e non viene scritto su disco; il testo non viene registrato. Del traffico restano solo, per chiave: secondi, richieste, tempi. Nessun dato lascia l'Unione Europea.